diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index 699d286..e699415 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -117,3 +117,33 @@ window.backgroundColor = rgba(160, 160, 160, 255) margin = Rect(0, 0, 0, 0) } % =================== + + + +% ====== Label ====== +(button) { + textColor = $textColor + backgroundColor = #999999FF + backgroundGradient = gradientH(#000000FF, #555555FF, #999999FF) + backgroundGradient = w_vgradientV(#000000FF, 0.2, #555555FF, 0.8, #999999FF) + borderColor = $accentColorDark + fontSize = 20 + borderRadius = 0 + borderWidth = 1 + showBackground = true + showBorder = true + useBackgroundGradient = true + padding = Rect(15, 5, 15, 5) + margin = Rect(0, 0, 0, 0) + icon = file("testIcon.png") +} +(button:hover) { + borderColor = $accentColor + backgroundColor = #BBBBBBFF +} +(button:pressed) { + borderColor = #BBBBBBFF + textColor = #DDDDDDFF + backgroundColor = #555555FF +} +% =================== diff --git a/src/ogfx/gui/Widgets.hpp b/src/ogfx/gui/Widgets.hpp index 391c598..e35ca47 100644 --- a/src/ogfx/gui/Widgets.hpp +++ b/src/ogfx/gui/Widgets.hpp @@ -24,3 +24,4 @@ #include #include #include +#include diff --git a/src/ogfx/gui/widgets/Button.cpp b/src/ogfx/gui/widgets/Button.cpp index fea4188..2557cfc 100644 --- a/src/ogfx/gui/widgets/Button.cpp +++ b/src/ogfx/gui/widgets/Button.cpp @@ -27,6 +27,60 @@ namespace ogfx { namespace widgets { + Button& Button::create(const String& text) + { + setText(text); + setPadding({ 5, 5, 5, 5 }); + setTypeName("ogfx::gui::widgets::Button"); + disableDrawBox(); + disableChildren(); + enableBackground(false); + validate(); + return *this; + } + + void Button::applyTheme(const ostd::Stylesheet& theme) + { + setTextColor(getThemeValue(theme, "button.textColor", Colors::White)); + setBackGroundColor(getThemeValue(theme, "button.backgroundColor", Colors::Transparent)); + setFontSize(getThemeValue(theme, "button.fontSize", 20)); + setBorderRadius(getThemeValue(theme, "button.borderRadius", 10)); + setBorderWidth(getThemeValue(theme, "button.borderWidth", 2)); + enableBorder(getThemeValue(theme, "button.showBorder", false)); + setBorderColor(getThemeValue(theme, "button.borderColor", Colors::White)); + enableBackground(getThemeValue(theme, "button.showBackground", false)); + setPadding(getThemeValue(theme, "button.padding", { 5, 5, 5, 5 })); + setMargin(getThemeValue(theme, "button.margin", { 0, 0, 0, 0 })); + m_useBackgroundGradient = getThemeValue(theme, "button.useBackgroundGradient", false); + if (m_useBackgroundGradient && isBackgoundEnabled()) + enableBackground(false); + } + + void Button::onDraw(ogfx::BasicRenderer2D& gfx) + { + if (m_textChanged) + __update_size(gfx); + if (m_useBackgroundGradient) + gfx.fillGradientRect(getGlobalBounds(), m_backgroundGradient); + gfx.drawString(getText(), getGlobalContentPosition(), getTextColor(), getFontSize()); + } + + void Button::setText(const String& text) + { + m_text = text; + m_textChanged = true; + } + + void Button::__update_size(ogfx::BasicRenderer2D& gfx) + { + auto size = gfx.getStringDimensions(getText(), getFontSize()); + size.x += getPadding().left(); + size.x += getPadding().right(); + size.y += getPadding().top(); + size.y += getPadding().bottom(); + setSize({ cast(size.x), cast(size.y) }); + m_textChanged = false; + } } } } diff --git a/src/ogfx/gui/widgets/Button.hpp b/src/ogfx/gui/widgets/Button.hpp index a29db42..569a9ca 100644 --- a/src/ogfx/gui/widgets/Button.hpp +++ b/src/ogfx/gui/widgets/Button.hpp @@ -28,6 +28,35 @@ namespace ogfx { namespace widgets { + class Button : public Widget + { + public: + inline Button(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); } + inline Button(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); } + Button& create(const String& text); + void applyTheme(const ostd::Stylesheet& theme) override; + void onDraw(ogfx::BasicRenderer2D& gfx) override; + void setText(const String& text); + inline String getText(void) const { return m_text; } + inline Color getTextColor(void) const { return m_textColor; } + inline void setTextColor(const Color& color) { m_textColor = color; } + inline i32 getFontSize(void) const { return m_fontSize; } + inline void setFontSize(i32 fontSize) { m_fontSize = fontSize; } + + private: + void __update_size(ogfx::BasicRenderer2D& gfx); + + private: + String m_text { "" }; + bool m_textChanged { false }; + i32 m_fontSize { 20 }; + Color m_textColor { 255, 255, 255 }; + bool m_useBackgroundGradient { false }; + ColorGradient m_backgroundGradient { + { Color { 160, 160, 160 }, Color { 120, 120, 120 } }, + { 1.0f } + }; + }; } } } diff --git a/src/ogfx/render/BasicRenderer.cpp b/src/ogfx/render/BasicRenderer.cpp index 5263782..18be3f9 100644 --- a/src/ogfx/render/BasicRenderer.cpp +++ b/src/ogfx/render/BasicRenderer.cpp @@ -314,6 +314,68 @@ namespace ogfx drawImage(img, position, size, anim.getFrameRect()); } + void BasicRenderer2D::fillGradientRect(const Rectangle& rect, ColorGradient& gradient) + { + auto& colors = gradient.getColors(); + auto& weights = gradient.getWeights(); + f32 angle_radians = gradient.getAngleRad(); + if (!m_initialized || colors.size() < 2) return; + if (weights.size() != colors.size() - 1) return; // N colors = N-1 weights + + // Normalize weights + f32 total = 0; + for (auto w : weights) total += w; + for (auto& w : weights) w /= total; + + // Compute cumulative positions along gradient (0 to 1) + stdvec stops(colors.size()); + stops[0] = 0.0f; + for (size_t i = 0; i < weights.size(); i++) + stops[i + 1] = stops[i] + weights[i]; + + // Center of rect for rotation + Vec2 center = { rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f }; + f32 cs = std::cos(angle_radians); + f32 sn = std::sin(angle_radians); + + auto rotate = [&](Vec2 p) -> Vec2 { + p -= center; + return { center.x + p.x * cs - p.y * sn, + center.y + p.x * sn + p.y * cs }; + }; + + // For each color band, push a quad + for (size_t i = 0; i < colors.size() - 1; i++) + { + f32 y0 = rect.y + rect.h * stops[i]; + f32 y1 = rect.y + rect.h * stops[i + 1]; + + Vec2 tl = rotate({ rect.x, y0 }); + Vec2 tr = rotate({ rect.x + rect.w, y0 }); + Vec2 br = rotate({ rect.x + rect.w, y1 }); + Vec2 bl = rotate({ rect.x, y1 }); + + if (m_vertexCount + 4 >= MaxVertices || m_indexCount + 6 >= MaxIndices) + flushBatch(); + if (m_texture != nullptr) + flushBatch(); + m_texture = nullptr; + + SDL_FColor c0 = COLOR_CAST(colors[i]); + SDL_FColor c1 = COLOR_CAST(colors[i + 1]); + + i32 base = m_vertexCount; + m_vertices[m_vertexCount++] = { { tl.x, tl.y }, c0, { 0, 0 } }; + m_vertices[m_vertexCount++] = { { tr.x, tr.y }, c0, { 0, 0 } }; + m_vertices[m_vertexCount++] = { { br.x, br.y }, c1, { 0, 0 } }; + m_vertices[m_vertexCount++] = { { bl.x, bl.y }, c1, { 0, 0 } }; + + u32 inds[6] = QUAD_INDICES_ARR; + for (i32 j = 0; j < 6; j++) + m_indices[m_indexCount++] = base + inds[j]; + } + } + void BasicRenderer2D::drawString(const String& str, const Vec2& position, const Color& color, i32 fontSize, f32 scale) { if (!isValid()) return; @@ -398,7 +460,7 @@ namespace ogfx if (!rounded || thickness < 4) return; - i32 segments = std::max(6, i32(thickness * 0.75f)); + i32 segments = std::max(16, i32(thickness * 1.5f)); generate_half_circle(p1, -dir, half, segments, color); generate_half_circle(p2, dir, half, segments, color); } @@ -475,7 +537,7 @@ namespace ogfx drawLine({ {x1, y2 - rBL}, {x1, y1 + rTL} }, color, thickness, false); // left // Corner arcs - auto segments = [](f32 r) -> i32 { return std::max(6, i32(r * 0.75f)); }; + auto segments = [](f32 r) -> i32 { return std::max(16, i32(r * 1.5f)); }; if (rTL > 0) generate_ellipse_stroke({ x1 + rTL, y1 + rTL }, rTL, rTL, thickness, M_PI, M_PI * 1.5f, color, segments(rTL)); if (rTR > 0) generate_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, thickness, M_PI * 1.5f, M_PI * 2.0f, color, segments(rTR)); @@ -487,7 +549,7 @@ namespace ogfx { if (!m_initialized || thickness <= 0) return; - i32 segments = std::max(12, i32(radius * 0.75f)); + i32 segments = std::max(16, i32(radius * 1.5f)); generate_ellipse_stroke(center, radius, radius, thickness, 0.0f, 2.0f * M_PI, color, segments); } @@ -513,8 +575,7 @@ namespace ogfx Vec2 center = { rect.x + rect.w*0.5f, rect.y + rect.h*0.5f }; f32 rx = rect.w * 0.5f; f32 ry = rect.h * 0.5f; - - i32 segments = std::max(12, i32(std::max(rx, ry) * 0.75f)); + i32 segments = std::max(16, i32(std::max(rx, ry) * 1.5f)); generate_ellipse_stroke(center, rx, ry, thickness, 0.0f, 2.0f * M_PI, color, segments); } @@ -723,7 +784,7 @@ namespace ogfx } // Corner fills - auto segments = [](f32 r) -> i32 { return std::max(6, i32(r * 0.75f)); }; + auto segments = [](f32 r) -> i32 { return std::max(16, i32(r * 1.5f)); }; if (rTL > 0) generate_filled_ellipse_stroke({ x1 + rTL, y1 + rTL }, rTL, rTL, M_PI, color, segments(rTL)); if (rTR > 0) generate_filled_ellipse_stroke({ x2 - rTR, y1 + rTR }, rTR, rTR, M_PI * 1.5f, color, segments(rTR)); @@ -736,7 +797,7 @@ namespace ogfx if (!m_initialized) return; - i32 segments = std::max(12, i32(radius * 0.75f)); + i32 segments = std::max(16, i32(radius * 1.5f)); generate_filled_ellipse(center, radius, radius, color, segments); } @@ -767,7 +828,7 @@ namespace ogfx f32 radiusX = rect.w * 0.5f; f32 radiusY = rect.h * 0.5f; - i32 segments = std::max(12, i32(std::max(radiusX, radiusY) * 0.75f)); + i32 segments = std::max(16, i32(std::max(radiusX, radiusY) * 1.5f)); generate_filled_ellipse(center, radiusX, radiusY, color, segments); } diff --git a/src/ogfx/render/BasicRenderer.hpp b/src/ogfx/render/BasicRenderer.hpp index 203461c..69d6734 100644 --- a/src/ogfx/render/BasicRenderer.hpp +++ b/src/ogfx/render/BasicRenderer.hpp @@ -82,6 +82,7 @@ namespace ogfx void drawImage(const ogfx::Image& image, const Vec2& position, const Vec2& size = { 0, 0 }, const Rectangle& srcRect = { 0, 0, 0, 0 }); void drawAnimation(const Animation& anim, const Vec2& position, const Vec2& size = { 0, 0 }); + void fillGradientRect(const Rectangle& rect, ColorGradient& gradient); void drawString(const String& str, const Vec2& position, const Color& color, i32 fontSize = 0, f32 scale = 1.0f); void drawCenteredString(const String& str, const Vec2& center, const Color& color, i32 fontSize = 0, f32 scale = 1.0f); diff --git a/src/ostd/data/Color.hpp b/src/ostd/data/Color.hpp index 1e7d05b..bd30835 100755 --- a/src/ostd/data/Color.hpp +++ b/src/ostd/data/Color.hpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace ostd { @@ -98,6 +99,31 @@ namespace ostd public: }; + class ColorGradient : public BaseObject + { + public: + inline ColorGradient(void) { } + inline ColorGradient(const stdvec& colors, const stdvec& weights) { m_colors = colors; m_weights = weights; } + inline void addColor(const Color& color) { m_colors.push_back(color); } + inline void addWeight(f32 w) { m_weights.push_back(w); } + inline stdvec& getColors(void) { return m_colors; } + inline stdvec& getWeights(void) { return m_weights; } + inline void setAngleDeg(f32 angle_degrees) { m_angleDeg = angle_degrees; } + inline void setAngleRad(f32 angle_radians) { m_angleDeg = RAD_TO_DEG(angle_radians); } + inline f32 getAngleDeg(void) const { return m_angleDeg; } + inline f32 getAngleRad(void) const { return DEG_TO_RAD(m_angleDeg); } + + private: + stdvec m_colors; + stdvec m_weights; + float m_angleDeg { VerticalDeg }; + + public: + inline static constexpr f32 HorizontalDeg { 90.0f }; + inline static constexpr f32 VerticalDeg { 0.0f }; + inline static constexpr f32 DiagonalDeg { 45.0f }; + inline static constexpr f32 Diagonal2Deg { 135.0f }; + }; struct Colors { inline static const Color Transparent { 0, 0, 0, 0 }; @@ -159,4 +185,5 @@ using Colors = ostd::Colors; namespace ogfx { using Color = ostd::Color; + using ColorGradient = ostd::ColorGradient; } diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index f476e18..f8239d9 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -24,6 +24,7 @@ #include ostd::ConsoleOutputHandler out; +using namespace ogfx::gui::widgets; class Window : public ogfx::gui::Window { @@ -63,10 +64,15 @@ class Window : public ogfx::gui::Window m_check1.setText("Check this out!"); m_check1.setChecked(true); - m_check1.setStateChangedCallback([&](ogfx::gui::widgets::CheckBox& sender, bool state) -> void { + m_check1.setStateChangedCallback([&](CheckBox& sender, bool state) -> void { m_panel1.setVisible(state); }); + m_btn1.setText("BUTTON"); + m_btn1.setMousePressedCallback([&](const ogfx::gui::Event& event) -> void { + + }); + m_label2.setText("Label2"); m_label3.setText("Label3"); @@ -80,6 +86,7 @@ class Window : public ogfx::gui::Window m_panel2.addWidget(m_label3); m_panel2.addWidget(m_panel1, { 400, 50 }); m_panel2.addWidget(m_label1, { 0, 500 }); + m_panel2.addWidget(m_btn1, { 0, 300 }); addWidget(m_check1, { 30, 30 }); addWidget(m_panel2, { 30, 100 }); @@ -101,6 +108,12 @@ class Window : public ogfx::gui::Window void onRedraw(ogfx::BasicRenderer2D& gfx) override { // gfx.drawAnimation(m_anim, { 200, 200 }); + // static f32 angle = 0.0f; + // angle += 0.5f; // degrees per frame, adjust to taste + // if (angle >= 360.0f) angle = 0.0f; + // m_grad.setAngleDeg(angle); + + // gfx.fillGradientRect({ 200, 200, 300, 300 }, m_grad); } void onFixedUpdate(void) override @@ -109,16 +122,28 @@ class Window : public ogfx::gui::Window } private: - ogfx::gui::widgets::Label m_label1 { *this }; - ogfx::gui::widgets::Label m_label2 { *this }; - ogfx::gui::widgets::Label m_label3 { *this }; - ogfx::gui::widgets::Panel m_panel1 { *this }; - ogfx::gui::widgets::Panel m_panel2 { *this }; - ogfx::gui::widgets::CheckBox m_check1 { *this }; + Label m_label1 { *this }; + Label m_label2 { *this }; + Label m_label3 { *this }; + Panel m_panel1 { *this }; + Panel m_panel2 { *this }; + CheckBox m_check1 { *this }; + Button m_btn1 { *this }; ostd::Stylesheet m_theme; ogfx::Animation m_anim; ogfx::Image m_img; + ogfx::ColorGradient m_grad { { + { 255, 0, 0 }, // red + { 255, 165, 0 }, // orange + { 255, 255, 0 }, // yellow + { 0, 255, 0 }, // green + { 0, 0, 255 }, // blue + { 148, 0, 211 }, // violet + { 255, 0, 0 }, // red again to loop cleanly + }, + { 1, 1, 1, 1, 1, 1 }, // equal weights, gets normalized + }; }; i32 main(i32 argc, char** argv)