Extra2D/src/ui/button.cpp

692 lines
19 KiB
C++
Raw Normal View History

2026-02-11 19:40:26 +08:00
#include <algorithm>
#include <app/application.h>
#include <cmath>
#include <core/string.h>
#include <graphics/render_backend.h>
#include <ui/button.h>
2026-02-11 19:40:26 +08:00
namespace extra2d {
// ============================================================================
// Button 实现
// ============================================================================
/**
* @brief
*/
2026-02-11 19:40:26 +08:00
Button::Button() {
setAnchor(0.0f, 0.0f);
setSpatialIndexed(false);
2026-02-11 19:40:26 +08:00
auto &dispatcher = getEventDispatcher();
dispatcher.addListener(EventType::UIHoverEnter, [this](Event &) {
hovered_ = true;
auto &app = Application::instance();
app.window().setCursor(hoverCursor_);
cursorChanged_ = true;
});
dispatcher.addListener(EventType::UIHoverExit, [this](Event &) {
hovered_ = false;
pressed_ = false;
if (cursorChanged_) {
auto &app = Application::instance();
app.window().resetCursor();
cursorChanged_ = false;
}
});
dispatcher.addListener(EventType::UIPressed,
[this](Event &) { pressed_ = true; });
dispatcher.addListener(EventType::UIReleased,
[this](Event &) { pressed_ = false; });
dispatcher.addListener(EventType::UIClicked, [this](Event &) {
if (toggleMode_) {
toggle();
}
if (onClick_) {
2026-02-11 19:40:26 +08:00
onClick_();
}
2026-02-11 19:40:26 +08:00
});
}
/**
* @brief
* @param text
*/
Button::Button(const std::string &text) : Button() { text_ = text; }
2026-02-11 19:40:26 +08:00
// ------------------------------------------------------------------------
// 静态创建方法
// ------------------------------------------------------------------------
/**
* @brief
* @return
*/
Ptr<Button> Button::create() { return makePtr<Button>(); }
2026-02-11 19:40:26 +08:00
/**
* @brief
* @param text
* @return
*/
Ptr<Button> Button::create(const std::string &text) {
return makePtr<Button>(text);
2026-02-11 19:40:26 +08:00
}
/**
* @brief
* @param text
* @param font
* @return
*/
2026-02-11 19:40:26 +08:00
Ptr<Button> Button::create(const std::string &text, Ptr<FontAtlas> font) {
auto btn = makePtr<Button>(text);
btn->setFont(font);
return btn;
}
// ------------------------------------------------------------------------
// 普通设置方法
// ------------------------------------------------------------------------
/**
* @brief
* @param text
*/
2026-02-11 19:40:26 +08:00
void Button::setText(const std::string &text) {
text_ = text;
if (font_ && size().empty()) {
2026-02-11 19:40:26 +08:00
Vec2 textSize = font_->measureText(text_);
setSize(textSize.x + padding_.x * 2.0f, textSize.y + padding_.y * 2.0f);
}
}
/**
* @brief
* @param font
*/
2026-02-11 19:40:26 +08:00
void Button::setFont(Ptr<FontAtlas> font) {
font_ = font;
if (font_ && size().empty() && !text_.empty()) {
2026-02-11 19:40:26 +08:00
Vec2 textSize = font_->measureText(text_);
setSize(textSize.x + padding_.x * 2.0f, textSize.y + padding_.y * 2.0f);
}
}
/**
* @brief Vec2版本
* @param padding
*/
2026-02-11 19:40:26 +08:00
void Button::setPadding(const Vec2 &padding) {
padding_ = padding;
if (font_ && size().empty() && !text_.empty()) {
2026-02-11 19:40:26 +08:00
Vec2 textSize = font_->measureText(text_);
setSize(textSize.x + padding_.x * 2.0f, textSize.y + padding_.y * 2.0f);
}
}
/**
* @brief
* @param x X方向内边距
* @param y Y方向内边距
*/
void Button::setPadding(float x, float y) { setPadding(Vec2(x, y)); }
/**
* @brief
* @param color
*/
void Button::setTextColor(const Color &color) { textColor_ = color; }
2026-02-11 19:40:26 +08:00
/**
* @brief
* @param normal
* @param hover
* @param pressed
*/
2026-02-11 19:40:26 +08:00
void Button::setBackgroundColor(const Color &normal, const Color &hover,
const Color &pressed) {
bgNormal_ = normal;
bgHover_ = hover;
bgPressed_ = pressed;
}
/**
* @brief
* @param color
* @param width
*/
2026-02-11 19:40:26 +08:00
void Button::setBorder(const Color &color, float width) {
borderColor_ = color;
borderWidth_ = width;
}
/**
* @brief
* @param radius
*/
2026-02-11 19:40:26 +08:00
void Button::setCornerRadius(float radius) {
cornerRadius_ = std::max(0.0f, radius);
}
/**
* @brief
* @param enabled
*/
2026-02-11 19:40:26 +08:00
void Button::setRoundedCornersEnabled(bool enabled) {
roundedCornersEnabled_ = enabled;
}
/**
* @brief 使Alpha遮罩进行点击检测
* @param enabled
*/
2026-02-11 19:40:26 +08:00
void Button::setUseAlphaMaskForHitTest(bool enabled) {
useAlphaMaskForHitTest_ = enabled;
}
/**
* @brief
* @param callback
*/
2026-02-11 19:40:26 +08:00
void Button::setOnClick(Function<void()> callback) {
onClick_ = std::move(callback);
}
/**
* @brief
* @param enabled
*/
void Button::setToggleMode(bool enabled) { toggleMode_ = enabled; }
/**
* @brief
* @param on
*/
void Button::setOn(bool on) {
if (isOn_ != on) {
isOn_ = on;
if (onStateChange_) {
onStateChange_(isOn_);
}
}
}
/**
* @brief
*/
void Button::toggle() { setOn(!isOn_); }
/**
* @brief
* @param callback
*/
void Button::setOnStateChange(Function<void(bool)> callback) {
onStateChange_ = std::move(callback);
}
/**
* @brief
* @param textOff
* @param textOn
*/
void Button::setStateText(const std::string &textOff,
const std::string &textOn) {
textOff_ = textOff;
textOn_ = textOn;
useStateText_ = true;
}
/**
* @brief
* @param colorOff
* @param colorOn
*/
void Button::setStateTextColor(const Color &colorOff, const Color &colorOn) {
textColorOff_ = colorOff;
textColorOn_ = colorOn;
useStateTextColor_ = true;
2026-02-11 19:40:26 +08:00
}
/**
* @brief
* @param cursor
*/
void Button::setHoverCursor(CursorShape cursor) { hoverCursor_ = cursor; }
/**
* @brief
* @param normal
* @param hover
* @param pressed
*/
2026-02-11 19:40:26 +08:00
void Button::setBackgroundImage(Ptr<Texture> normal, Ptr<Texture> hover,
Ptr<Texture> pressed) {
imgNormal_ = normal;
imgHover_ = hover ? hover : normal;
imgPressed_ = pressed ? pressed : (hover ? hover : normal);
useImageBackground_ = (normal != nullptr);
useTextureRect_ = false;
2026-02-11 19:40:26 +08:00
if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original && normal) {
setSize(static_cast<float>(normal->width()),
static_cast<float>(normal->height()));
2026-02-11 19:40:26 +08:00
}
}
/**
* @brief
* @param texture
* @param rect
*/
void Button::setBackgroundImage(Ptr<Texture> texture, const Rect &rect) {
imgNormal_ = texture;
imgHover_ = texture;
imgPressed_ = texture;
imgNormalRect_ = rect;
imgHoverRect_ = rect;
imgPressedRect_ = rect;
useImageBackground_ = (texture != nullptr);
useTextureRect_ = true;
useStateImages_ = false;
if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original) {
setSize(rect.size.width, rect.size.height);
}
}
/**
* @brief
* @param offNormal
* @param onNormal
* @param offHover
* @param onHover
* @param offPressed
* @param onPressed
*/
void Button::setStateBackgroundImage(
Ptr<Texture> offNormal, Ptr<Texture> onNormal, Ptr<Texture> offHover,
Ptr<Texture> onHover, Ptr<Texture> offPressed, Ptr<Texture> onPressed) {
imgOffNormal_ = offNormal;
imgOnNormal_ = onNormal;
imgOffHover_ = offHover ? offHover : offNormal;
imgOnHover_ = onHover ? onHover : onNormal;
imgOffPressed_ = offPressed ? offPressed : offNormal;
imgOnPressed_ = onPressed ? onPressed : onNormal;
useStateImages_ = (offNormal != nullptr && onNormal != nullptr);
useImageBackground_ = useStateImages_;
useTextureRect_ = false;
if (useStateImages_ && scaleMode_ == ImageScaleMode::Original && offNormal) {
setSize(static_cast<float>(offNormal->width()),
static_cast<float>(offNormal->height()));
}
}
/**
* @brief
* @param mode
*/
2026-02-11 19:40:26 +08:00
void Button::setBackgroundImageScaleMode(ImageScaleMode mode) {
scaleMode_ = mode;
if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original &&
imgNormal_) {
setSize(static_cast<float>(imgNormal_->width()),
static_cast<float>(imgNormal_->height()));
2026-02-11 19:40:26 +08:00
}
}
/**
* @brief Vec2版本
* @param size
*/
void Button::setCustomSize(const Vec2 &size) { setSize(size.x, size.y); }
2026-02-11 19:40:26 +08:00
/**
* @brief
* @param width
* @param height
*/
2026-02-11 19:40:26 +08:00
void Button::setCustomSize(float width, float height) {
setSize(width, height);
}
/**
* @brief
* @return
*/
Rect Button::boundingBox() const {
auto position = convertToWorldSpace(extra2d::Vec2::Zero());
auto anchorPt = anchor();
auto scaleVal = scale();
auto widgetSize = size();
2026-02-11 19:40:26 +08:00
if (widgetSize.empty()) {
2026-02-11 19:40:26 +08:00
return Rect();
}
float w = widgetSize.width * scaleVal.x;
float h = widgetSize.height * scaleVal.y;
float x0 = position.x - widgetSize.width * anchorPt.x * scaleVal.x;
float y0 = position.y - widgetSize.height * anchorPt.y * scaleVal.y;
2026-02-11 19:40:26 +08:00
return Rect(x0, y0, w, h);
}
/**
* @brief
* @param buttonSize
* @param imageSize
* @return
*/
2026-02-11 19:40:26 +08:00
Vec2 Button::calculateImageSize(const Vec2 &buttonSize, const Vec2 &imageSize) {
switch (scaleMode_) {
case ImageScaleMode::Original:
return imageSize;
case ImageScaleMode::Stretch:
return buttonSize;
case ImageScaleMode::ScaleFit: {
float scaleX = buttonSize.x / imageSize.x;
float scaleY = buttonSize.y / imageSize.y;
float scale = std::min(scaleX, scaleY);
return Vec2(imageSize.x * scale, imageSize.y * scale);
}
case ImageScaleMode::ScaleFill: {
float scaleX = buttonSize.x / imageSize.x;
float scaleY = buttonSize.y / imageSize.y;
float scale = std::max(scaleX, scaleY);
return Vec2(imageSize.x * scale, imageSize.y * scale);
}
}
return imageSize;
}
/**
* @brief
* @param renderer
* @param rect
*/
void Button::drawBackgroundImage(Renderer &renderer, const Rect &rect) {
2026-02-11 19:40:26 +08:00
Texture *texture = nullptr;
Rect srcRect;
if (useStateImages_) {
if (isOn_) {
if (pressed_ && imgOnPressed_) {
texture = imgOnPressed_.get();
} else if (hovered_ && imgOnHover_) {
texture = imgOnHover_.get();
} else {
texture = imgOnNormal_.get();
}
} else {
if (pressed_ && imgOffPressed_) {
texture = imgOffPressed_.get();
} else if (hovered_ && imgOffHover_) {
texture = imgOffHover_.get();
} else {
texture = imgOffNormal_.get();
}
}
if (texture) {
srcRect = Rect(0, 0, static_cast<float>(texture->width()),
static_cast<float>(texture->height()));
}
} else {
if (pressed_ && imgPressed_) {
texture = imgPressed_.get();
srcRect = useTextureRect_
? imgPressedRect_
: Rect(0, 0, static_cast<float>(imgPressed_->width()),
static_cast<float>(imgPressed_->height()));
} else if (hovered_ && imgHover_) {
texture = imgHover_.get();
srcRect = useTextureRect_
? imgHoverRect_
: Rect(0, 0, static_cast<float>(imgHover_->width()),
static_cast<float>(imgHover_->height()));
} else if (imgNormal_) {
texture = imgNormal_.get();
srcRect = useTextureRect_
? imgNormalRect_
: Rect(0, 0, static_cast<float>(imgNormal_->width()),
static_cast<float>(imgNormal_->height()));
}
2026-02-11 19:40:26 +08:00
}
if (!texture)
return;
Vec2 imageSize(srcRect.size.width, srcRect.size.height);
2026-02-11 19:40:26 +08:00
Vec2 buttonSize(rect.size.width, rect.size.height);
Vec2 drawSize = calculateImageSize(buttonSize, imageSize);
Vec2 drawPos(rect.origin.x + (rect.size.width - drawSize.x) * 0.5f,
rect.origin.y + (rect.size.height - drawSize.y) * 0.5f);
Rect destRect(drawPos.x, drawPos.y, drawSize.x, drawSize.y);
renderer.drawSprite(*texture, destRect, srcRect, Colors::White, 0.0f,
Vec2::Zero());
2026-02-11 19:40:26 +08:00
}
/**
* @brief
* @param renderer
* @param rect
* @param color
* @param radius
*/
void Button::drawRoundedRect(Renderer &renderer, const Rect &rect,
2026-02-11 19:40:26 +08:00
const Color &color, float radius) {
float maxRadius = std::min(rect.size.width, rect.size.height) * 0.5f;
radius = std::min(radius, maxRadius);
if (radius <= 0.0f) {
renderer.drawRect(rect, color, borderWidth_);
return;
}
const int segments = 8;
float x = rect.origin.x;
float y = rect.origin.y;
float w = rect.size.width;
float h = rect.size.height;
float r = radius;
renderer.drawLine(Vec2(x + r, y), Vec2(x + w - r, y), color, borderWidth_);
renderer.drawLine(Vec2(x + r, y + h), Vec2(x + w - r, y + h), color,
borderWidth_);
2026-02-11 19:40:26 +08:00
renderer.drawLine(Vec2(x, y + r), Vec2(x, y + h - r), color, borderWidth_);
renderer.drawLine(Vec2(x + w, y + r), Vec2(x + w, y + h - r), color,
borderWidth_);
2026-02-11 19:40:26 +08:00
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments + 3.14159f;
float angle2 = 3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f;
Vec2 p1(x + r + r * cosf(angle1), y + r + r * sinf(angle1));
Vec2 p2(x + r + r * cosf(angle2), y + r + r * sinf(angle2));
renderer.drawLine(p1, p2, color, borderWidth_);
}
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments + 3.14159f * 1.5f;
float angle2 =
3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f * 1.5f;
2026-02-11 19:40:26 +08:00
Vec2 p1(x + w - r + r * cosf(angle1), y + r + r * sinf(angle1));
Vec2 p2(x + w - r + r * cosf(angle2), y + r + r * sinf(angle2));
renderer.drawLine(p1, p2, color, borderWidth_);
}
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments;
float angle2 = 3.14159f * 0.5f * (float)(i + 1) / segments;
Vec2 p1(x + w - r + r * cosf(angle1), y + h - r + r * sinf(angle1));
Vec2 p2(x + w - r + r * cosf(angle2), y + h - r + r * sinf(angle2));
renderer.drawLine(p1, p2, color, borderWidth_);
}
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments + 3.14159f * 0.5f;
float angle2 =
3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f * 0.5f;
2026-02-11 19:40:26 +08:00
Vec2 p1(x + r + r * cosf(angle1), y + h - r + r * sinf(angle1));
Vec2 p2(x + r + r * cosf(angle2), y + h - r + r * sinf(angle2));
renderer.drawLine(p1, p2, color, borderWidth_);
}
}
/**
* @brief
* @param renderer
* @param rect
* @param color
* @param radius
*/
void Button::fillRoundedRect(Renderer &renderer, const Rect &rect,
2026-02-11 19:40:26 +08:00
const Color &color, float radius) {
float maxRadius = std::min(rect.size.width, rect.size.height) * 0.5f;
radius = std::min(radius, maxRadius);
if (radius <= 0.0f) {
renderer.fillRect(rect, color);
return;
}
const int segments = 8;
float x = rect.origin.x;
float y = rect.origin.y;
float w = rect.size.width;
float h = rect.size.height;
float r = radius;
std::vector<Vec2> vertices;
vertices.push_back(Vec2(x + r, y + r));
vertices.push_back(Vec2(x + w - r, y + r));
vertices.push_back(Vec2(x + w - r, y + h - r));
vertices.push_back(Vec2(x + r, y + h - r));
renderer.fillPolygon(vertices, color);
renderer.fillRect(Rect(x + r, y, w - 2 * r, r), color);
renderer.fillRect(Rect(x + r, y + h - r, w - 2 * r, r), color);
renderer.fillRect(Rect(x, y + r, r, h - 2 * r), color);
renderer.fillRect(Rect(x + w - r, y + r, r, h - 2 * r), color);
vertices.clear();
vertices.push_back(Vec2(x + r, y + r));
for (int i = 0; i <= segments; i++) {
float angle = 3.14159f + 3.14159f * 0.5f * (float)i / segments;
vertices.push_back(Vec2(x + r + r * cosf(angle), y + r + r * sinf(angle)));
}
renderer.fillPolygon(vertices, color);
vertices.clear();
vertices.push_back(Vec2(x + w - r, y + r));
for (int i = 0; i <= segments; i++) {
float angle = 3.14159f * 1.5f + 3.14159f * 0.5f * (float)i / segments;
vertices.push_back(
Vec2(x + w - r + r * cosf(angle), y + r + r * sinf(angle)));
}
renderer.fillPolygon(vertices, color);
vertices.clear();
vertices.push_back(Vec2(x + w - r, y + h - r));
for (int i = 0; i <= segments; i++) {
float angle = 0 + 3.14159f * 0.5f * (float)i / segments;
vertices.push_back(
Vec2(x + w - r + r * cosf(angle), y + h - r + r * sinf(angle)));
}
renderer.fillPolygon(vertices, color);
vertices.clear();
vertices.push_back(Vec2(x + r, y + h - r));
for (int i = 0; i <= segments; i++) {
float angle = 3.14159f * 0.5f + 3.14159f * 0.5f * (float)i / segments;
vertices.push_back(
Vec2(x + r + r * cosf(angle), y + h - r + r * sinf(angle)));
}
renderer.fillPolygon(vertices, color);
}
/**
* @brief
* @param renderer
*/
void Button::onDrawWidget(Renderer &renderer) {
Rect rect = boundingBox();
2026-02-11 19:40:26 +08:00
if (rect.empty()) {
return;
}
if (useImageBackground_) {
drawBackgroundImage(renderer, rect);
} else {
renderer.endSpriteBatch();
Color bg = bgNormal_;
if (pressed_) {
bg = bgPressed_;
} else if (hovered_) {
bg = bgHover_;
}
if (roundedCornersEnabled_) {
fillRoundedRect(renderer, rect, bg, cornerRadius_);
} else {
renderer.fillRect(rect, bg);
}
renderer.beginSpriteBatch();
renderer.endSpriteBatch();
2026-02-11 19:40:26 +08:00
if (borderWidth_ > 0.0f) {
if (roundedCornersEnabled_) {
drawRoundedRect(renderer, rect, borderColor_, cornerRadius_);
} else {
renderer.drawRect(rect, borderColor_, borderWidth_);
}
2026-02-11 19:40:26 +08:00
}
renderer.beginSpriteBatch();
}
2026-02-11 19:40:26 +08:00
if (font_) {
2026-02-11 19:40:26 +08:00
std::string textToDraw;
if (useStateText_) {
textToDraw = isOn_ ? textOn_ : textOff_;
} else {
textToDraw = text_;
2026-02-11 19:40:26 +08:00
}
Color colorToUse;
if (useStateTextColor_) {
colorToUse = isOn_ ? textColorOn_ : textColorOff_;
} else {
colorToUse = textColor_;
2026-02-11 19:40:26 +08:00
}
if (!textToDraw.empty()) {
Vec2 textSize = font_->measureText(textToDraw);
2026-02-11 19:40:26 +08:00
Vec2 textPos(rect.center().x - textSize.x * 0.5f,
rect.center().y - textSize.y * 0.5f);
float minX = rect.left() + padding_.x;
float minY = rect.top() + padding_.y;
float maxX = rect.right() - padding_.x - textSize.x;
float maxY = rect.bottom() - padding_.y - textSize.y;
textPos.x = std::max(minX, std::min(textPos.x, maxX));
textPos.y = std::max(minY, std::min(textPos.y, maxY));
2026-02-11 19:40:26 +08:00
colorToUse.a = 1.0f;
renderer.drawText(*font_, textToDraw, textPos, colorToUse);
2026-02-11 19:40:26 +08:00
}
}
}
} // namespace extra2d