diff --git a/.clangd b/.clangd index c60968a..e6defe1 100644 --- a/.clangd +++ b/.clangd @@ -1,5 +1,4 @@ CompileFlags: CompilationDatabase: bin - Add: ["--target=x86_64-w64-mingw32"] Diagnostics: MissingIncludes: None \ No newline at end of file diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index 1f479ed..81d0ba7 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -196,7 +196,7 @@ const $accentColorDark = #6B0A1DFF showBackground = true showBorder = true useBackgroundGradient = true - padding = rect(15, 8, 15, 8) + padding = rect(5, 5, 5, 5) margin = rect(0, 0, 0, 0) showIcon = false (icon) { @@ -308,7 +308,28 @@ const $accentColorDark = #6B0A1DFF borderColor = #400000FF } % ===================== + + + +% ====== MenuBar ====== +(toolbar) { + height = 30.0 + fontSize = 16 + backgroundColor = #AA9999FF + textColor = $textColor + selectionColor = $accentColor + selectionTextColor = #FFFFFFFF + borderColor = #400000FF +} +% ===================== (@tool_button button) { fontSize = 18 - padding = rect(10, 0, 10, 0) + padding = rect(5, 0, 5, 0) + margin = rect(5, 0, 0, 0) + showBackground = true + backgroundColor = #FFAAAAAA + useBackgroundGradient = false + showBorder = true + borderColor = color_black + showIcon = true } diff --git a/src/ogfx/gui/ToolBar.cpp b/src/ogfx/gui/ToolBar.cpp index 84b466f..1a726e7 100644 --- a/src/ogfx/gui/ToolBar.cpp +++ b/src/ogfx/gui/ToolBar.cpp @@ -37,7 +37,9 @@ namespace ogfx setStylesheetCategoryName("toolbar"); setTypeName("ogfx::gui::widgets::ToolBar"); auto& win = cast(getWindow()); - setSize(win.getWindowWidth(), m_height); + w = win.getWindowWidth(); + h = m_height; + disableBorder(); if (win.isMenuBarVisible()) setPosition(0, win.getMenuBar().getHeight()); else @@ -45,10 +47,27 @@ namespace ogfx return *this; } + widgets::Button& ToolBar::addButton(const String& iconPath, const String& text, EventCallback callback) + { + static f32 pad = 0; + m_buttons.push_back({ getWindow() }); + auto& btn = m_buttons.back(); + btn.addThemeID("tool_button"); + btn.setIcon(iconPath); + btn.setIconSize({ m_height, m_height }); + btn.setText(text); + btn.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, callback); + btn.enableIconOnly(); + addWidget(btn, { m_height * (m_buttons.size() - 1) + pad, 0 }); + pad += 15; + return btn; + } + void ToolBar::onWindowResized(const Event& event) { auto& win = cast(getWindow()); - setSize(win.getWindowWidth(), m_height); + w = win.getWindowWidth(); + h = m_height; f32 offset_y = 0; if (win.getMenuBar().isVisible()) offset_y += win.getMenuBar().geth(); @@ -59,7 +78,6 @@ namespace ogfx { auto& win = cast(getWindow()); setHeight(theme.get("toolbar.height", getHeight(), {}, {})); - setItemPadding(theme.get("toolbar.itemPadding", getItemPadding(), {}, {})); setFontSize(theme.get("toolbar.fontSize", getFontSize(), {}, {})); setBackgroundColor(theme.get("toolbar.backgroundColor", getBackgroundColor(), {}, {})); setTextColor(theme.get("toolbar.textColor", getTextColor(), {}, {})); @@ -80,6 +98,9 @@ namespace ogfx void ToolBar::onUpdate(void) { if (!m_visible) return; + bool iconsOnly = !isButtonTextEnabled(); + for (auto& btn : m_buttons) + btn.enableIconOnly(iconsOnly); } void ToolBar::onMousePressed(const Event& event) @@ -98,5 +119,13 @@ namespace ogfx Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; } + + void ToolBar::refresh_size(void) + { + for (auto& btn : m_buttons) + { + btn.setIconSize({ m_height, m_height }); + } + } } } diff --git a/src/ogfx/gui/ToolBar.hpp b/src/ogfx/gui/ToolBar.hpp index 634c255..cba9b67 100644 --- a/src/ogfx/gui/ToolBar.hpp +++ b/src/ogfx/gui/ToolBar.hpp @@ -21,6 +21,8 @@ #pragma once #include +#include +#include namespace ogfx { @@ -32,6 +34,7 @@ namespace ogfx public: ToolBar(Window& window); ToolBar& create(void); + widgets::Button& addButton(const String& iconPath, const String& text = "", EventCallback callback = nullptr); void onWindowResized(const Event& event) override; void applyTheme(const ostd::Stylesheet& theme) override; void onDraw(BasicRenderer2D& gfx) override; @@ -39,22 +42,29 @@ namespace ogfx void onMousePressed(const Event& event) override; void onMouseMoved(const Event& event) override; + inline void setw(f32 ww) override { } + inline void seth(f32 hh) override { h = hh; refresh_size(); } + inline bool isVisible(void) const { return m_visible; } inline void show(bool v = true) { m_visible = v; } inline void hide(void) { show(false); } OSTD_PARAM_GETSET(f32, Height, m_height); - OSTD_PARAM_GETSET(Rectangle, ItemPadding, m_itemPadding); OSTD_PARAM_GETSET(i32, FontSize, m_fontSize); OSTD_PARAM_GETSET(Color, BackgroundColor, m_backgroundColor); OSTD_PARAM_GETSET(Color, TextColor, m_textColor); OSTD_PARAM_GETSET(Color, BorderColor, m_borderColor); + OSTD_BOOL_PARAM_GETSET_E_NEG(ButtonText, m_disableButtonText); + + private: + void refresh_size(void); private: bool m_visible { true }; + bool m_disableButtonText { true }; + std::deque m_buttons; f32 m_height { 26 }; - Rectangle m_itemPadding { 0, 0, 0, 0 }; i32 m_fontSize { 16 }; Color m_backgroundColor { "#6B0A1DFF" }; Color m_textColor { "#F16A85FF" }; diff --git a/src/ogfx/gui/widgets/Button.cpp b/src/ogfx/gui/widgets/Button.cpp index ee74388..6de202d 100644 --- a/src/ogfx/gui/widgets/Button.cpp +++ b/src/ogfx/gui/widgets/Button.cpp @@ -31,29 +31,45 @@ namespace ogfx { Button& Button::create(const String& text) { - setText(text); + m_text = text; setPadding({ 5, 5, 5, 5 }); setTypeName("ogfx::gui::widgets::Button"); disableChildren(); enableBackground(false); setStylesheetCategoryName("button"); validate(); + invalidate_layout(); return *this; } void Button::onDraw(ogfx::BasicRenderer2D& gfx) { - if (m_textChanged) - __update_size(); - if (isIconEnabled()) - { + if (m_layoutDirty) + recompute_layout(); + const Vec2 contentTL = getGlobalContentPosition(); + + // --- Icon --- + if (isIconEnabled() && m_drawIconSize.x > 0 && m_drawIconSize.y > 0) + { + const Vec2 iconPos = contentTL + m_drawIconOffset; if (isAnimatedEnabled()) - gfx.drawAnimation(m_anim, getGlobalContentPosition(), m_realIconSize); + gfx.drawAnimation(m_anim, iconPos, m_drawIconSize); else - gfx.drawImage(m_icon, getGlobalContentPosition(), m_realIconSize); + gfx.drawImage(m_icon, iconPos, m_drawIconSize); + } + + // --- Text --- + if (!isIconOnlyEnabled() && m_text.len() > 0) + { + gfx.drawString( + m_text, + contentTL + m_drawTextOffset, + getTextColor(), + getFontSize(), + m_contentScale + ); } - gfx.drawString(getText(), getGlobalContentPosition() + Vec2 { m_realIconSize.x + m_iconSpacing, 0 }, getTextColor(), getFontSize()); } void Button::onUpdate(void) @@ -73,55 +89,167 @@ namespace ogfx void Button::setText(const String& text) { m_text = text; - m_textChanged = true; - __update_size(); + invalidate_layout(); } void Button::setIcon(const String& filePath) { disableIcon(); - if (filePath == "") return; - if (!ostd::FileSystem::fileExists(filePath)) - return; + if (filePath == "") { invalidate_layout(); return; } + if (!ostd::FileSystem::fileExists(filePath)) { invalidate_layout(); return; } m_icon.destroy(); m_icon.loadFromFile(filePath, getWindow().getGFX()); enableIcon(); + invalidate_layout(); } void Button::applyTheme(const ostd::Stylesheet& theme) { enableIcon(getThemeValue(theme, "showIcon", m_showIcon)); + enableIconOnly(getThemeValue(theme, "iconOnly", m_iconOnly)); + enableAutoSize(getThemeValue(theme, "autoSize", m_autoSize)); enableAnimated(getThemeValue(theme, "icon.animated", m_animated)); setAnimationData(getThemeValue(theme, "icon.animation", m_animData)); String filePath = getThemeValue(theme, "icon.path", m_icon.getFilePath()); if (filePath != m_icon.getFilePath()) setIcon(filePath); setIconSize(getThemeValue(theme, "icon.size", getIconSize())); + setIconSpacing(getThemeValue(theme, "icon.spacing", m_iconSpacing)); if (isAnimatedEnabled()) { m_anim.create(m_animData); m_anim.setSpriteSheet(m_icon); } - __update_size(); + invalidate_layout(); } - void Button::__update_size(void) + Vec2 Button::native_icon_size(void) const { - m_realIconSize = getIconSize().propy(getGlobalContentBounds().getSize().y); - if (!isAutoSizeEnabled()) + if (!isIconEnabled()) + return { 0, 0 }; + + // Treat font size as the natural icon edge if the user didn't pick one. + const f32 fontEdge = cast(getFontSize()); + + Vec2 hint = m_iconSize; + if (hint.x <= 0 && hint.y <= 0) { - m_textChanged = false; - return; + if (m_icon.isLoaded()) + { + // Fit the icon's natural aspect ratio into a font-sized square. + Vec2 nat = m_icon.getSize(); + if (nat.x > 0 && nat.y > 0) + { + if (nat.x >= nat.y) + return nat.propx(fontEdge); + return nat.propy(fontEdge); + } + } + return { fontEdge, fontEdge }; } - auto size = getWindow().getGFX().getStringDimensions(getText(), getFontSize()); - size.x += getPadding().left(); - size.x += getPadding().right(); - size.y += getPadding().top(); - size.y += getPadding().bottom(); - if (isIconEnabled()) - size.x += m_iconSpacing + m_realIconSize.x; - setSize({ cast(size.x), cast(size.y) }); - m_textChanged = false; + + // One axis specified: derive the other from the icon's aspect (if any), + // otherwise make it square. + if (hint.x <= 0) + { + if (m_icon.isLoaded()) + { + Vec2 nat = m_icon.getSize(); + if (nat.x > 0 && nat.y > 0) + return nat.propy(hint.y); + } + return { hint.y, hint.y }; + } + if (hint.y <= 0) + { + if (m_icon.isLoaded()) + { + Vec2 nat = m_icon.getSize(); + if (nat.x > 0 && nat.y > 0) + return nat.propx(hint.x); + } + return { hint.x, hint.x }; + } + return hint; + } + + void Button::recompute_layout(void) + { + m_layoutDirty = false; + + auto& gfx = getWindow().getGFX(); + const Rectangle pad = getPadding(); + const f32 padW = pad.left() + pad.right(); + const f32 padH = pad.top() + pad.bottom(); + + // 1) Native (un-scaled) sizes of the pieces. + const Vec2 iconNative = native_icon_size(); + const bool hasIcon = isIconEnabled() && (iconNative.x > 0 && iconNative.y > 0); + const bool hasText = !isIconOnlyEnabled() && m_text.len() > 0; + const Vec2 textNative = hasText + ? gfx.getStringDimensions(m_text, getFontSize()) + : Vec2 { 0, 0 }; + + // 2) Native content extents (icon + spacing + text), no padding yet. + f32 nativeW = 0; + f32 nativeH = 0; + if (hasIcon) + { + nativeW += iconNative.x; + nativeH = std::max(nativeH, iconNative.y); + } + if (hasText) + { + if (hasIcon) nativeW += m_iconSpacing; + nativeW += textNative.x; + nativeH = std::max(nativeH, textNative.y); + } + if (nativeW <= 0) nativeW = cast(getFontSize()); // degenerate fallback + if (nativeH <= 0) nativeH = cast(getFontSize()); + + // 3) Decide the widget size and the content scale. + if (isAutoSizeEnabled()) + { + m_contentScale = 1.0f; + setSize({ nativeW + padW, nativeH + padH }); + } + else + { + // Honor the size the user already set on the widget. + const f32 availW = std::max(0.0f, getw() - padW); + const f32 availH = std::max(0.0f, geth() - padH); + if (availW <= 0 || availH <= 0) + m_contentScale = 0.0f; + else + { + const f32 sx = availW / nativeW; + const f32 sy = availH / nativeH; + m_contentScale = std::min(sx, sy); + } + } + + // 4) Compute scaled draw sizes and centered offsets within the content area. + const Vec2 contentSize = getContentBounds().getSize(); + const Vec2 iconScaled = hasIcon ? iconNative * m_contentScale : Vec2 { 0, 0 }; + const Vec2 textScaled = hasText ? textNative * m_contentScale : Vec2 { 0, 0 }; + const f32 spacing = (hasIcon && hasText) ? (m_iconSpacing * m_contentScale) : 0.0f; + + const f32 totalW = iconScaled.x + spacing + textScaled.x; + const f32 startX = std::max(0.0f, (contentSize.x - totalW) * 0.5f); + + m_drawIconSize = iconScaled; + if (hasIcon) + m_drawIconOffset = { startX, std::max(0.0f, (contentSize.y - iconScaled.y) * 0.5f) }; + else + m_drawIconOffset = { 0, 0 }; + + if (hasText) + m_drawTextOffset = { + startX + iconScaled.x + spacing, + std::max(0.0f, (contentSize.y - textScaled.y) * 0.5f) + }; + else + m_drawTextOffset = { 0, 0 }; } } } diff --git a/src/ogfx/gui/widgets/Button.hpp b/src/ogfx/gui/widgets/Button.hpp index c1c6bcb..c2c414b 100644 --- a/src/ogfx/gui/widgets/Button.hpp +++ b/src/ogfx/gui/widgets/Button.hpp @@ -44,28 +44,60 @@ namespace ogfx void applyTheme(const ostd::Stylesheet& theme) override; inline String getText(void) const { return m_text; } inline Image& getIcon(void) { return m_icon; } - OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated); + + // When auto-size is enabled, the widget resizes itself to fit text+icon+padding + // at native font/icon size (text is drawn at scale 1.0). + // When auto-size is disabled, the widget keeps its user-defined size, and the + // content is scaled uniformly down/up to fit that size (text uses the renderer's + // scale parameter; icon is scaled proportionally). OSTD_BOOL_PARAM_GETSET_E(AutoSize, m_autoSize); + + // Icon-only mode: text is ignored when laying out and drawing. + // Combined with auto-size, the button becomes a square based on font size. + // Combined with manual size, the icon scales to fit the content area. + OSTD_BOOL_PARAM_GETSET_E(IconOnly, m_iconOnly); + + OSTD_BOOL_PARAM_GETSET_E(Animated, m_animated); OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon); OSTD_PARAM_GETSET(ostd::AnimationData, AnimationData, m_animData); OSTD_PARAM_GETSET(Vec2, IconSize, m_iconSize); + OSTD_PARAM_GETSET(f32, IconSpacing, m_iconSpacing); private: - void __update_size(void); + // Force a layout recompute on the next draw. + inline void invalidate_layout(void) { m_layoutDirty = true; } + + // Recomputes layout. In auto-size mode this also resizes the widget. + // In manual-size mode this computes the scale factor that fits content + // into the current widget size. + void recompute_layout(void); + + // Returns the icon size at native (un-scaled) resolution. If m_iconSize is + // (0,0) and an icon is loaded, falls back to the icon's natural size; if no + // icon is loaded, falls back to font-size-based square. Aspect ratio of + // m_iconSize is preserved. + Vec2 native_icon_size(void) const; private: String m_text { "" }; - bool m_textChanged { true }; Image m_icon; - Vec2 m_realIconSize { 0, 0 }; - bool m_autoSize { true }; - - bool m_showIcon { false }; ostd::AnimationData m_animData; Animation m_anim; - bool m_animated { false }; - f32 m_iconSpacing { 10 }; Vec2 m_iconSize { 0, 0 }; + f32 m_iconSpacing { 10 }; + + // Computed layout state (refreshed by recompute_layout). + bool m_layoutDirty { true }; + f32 m_contentScale { 1.0f }; // applied to text & icon when !m_autoSize + Vec2 m_drawIconSize { 0, 0 }; // final on-screen icon size + Vec2 m_drawIconOffset { 0, 0 }; // top-left of the icon inside the content area + Vec2 m_drawTextOffset { 0, 0 }; // top-left of the text inside the content area + + // Flags + bool m_autoSize { true }; + bool m_iconOnly { false }; + bool m_showIcon { false }; + bool m_animated { false }; }; } } diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index c99ef01..20b72d2 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -50,6 +50,8 @@ class Window : public ogfx::gui::Window m_img.enableAnimated(); m_img.setSize(256, 256); + getToolBar().setHeight(30); + m_label1.setText("Show Panel2"); m_label1.setCallback(ogfx::gui::Widget::eCallback::MousePressed, [&](const ogfx::gui::Event& event) -> void { m_check1.setChecked(!m_check1.isChecked()); @@ -66,16 +68,6 @@ class Window : public ogfx::gui::Window m_label1.enableBackground(); m_label1.reloadTheme(); - m_check1.setText("Check this out!"); - m_check1.setChecked(true); - m_check1.setStateChangedCallback([&](CheckBox& sender, bool state) -> void { - m_panel1.setVisible(state); - }); - - m_btn1.setText("BUTTON"); - m_btn1.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, [&](const ogfx::gui::Event& event) -> void { - std::cout << showOpenFileDialog(filters) << "\n"; - }); m_btn1.addThemeOverride("@.button.showIcon", true); m_btn1.setAnimationData(ad); m_btn1.setIcon("./img.png"); @@ -83,6 +75,16 @@ class Window : public ogfx::gui::Window m_btn1.enableAnimated(); m_btn1.enableTooltip(); m_btn1.setTooltipText("Test tooltip"); + m_btn1.setText("TEST BUTTON"); + m_btn1.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, [&](const ogfx::gui::Event& event) -> void { + std::cout << showOpenFileDialog(filters) << "\n"; + }); + + m_check1.setText("Check this out!"); + m_check1.setChecked(true); + m_check1.setStateChangedCallback([&](CheckBox& sender, bool state) -> void { + m_panel1.setVisible(state); + }); m_label2.setText("Label2"); m_label3.setText("Label3"); @@ -132,6 +134,13 @@ class Window : public ogfx::gui::Window std::cout << *(selection[0]) << "\n"; }); + for (i32 i = 0; i < 20; i++) + { + auto& btn = getToolBar().addButton("./img.png"); + btn.setAnimationData(ad); + btn.enableAnimated(); + } + m_tabs.setSize(900, 700); auto& t1 = m_tabs.addTab("Tab1"); auto& t2 = m_tabs.addTab("Tab2 Test"); @@ -163,12 +172,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 }); - m_btn1.addThemeID("tool_button"); - m_btn1.seth(26); - m_btn1.setText(" "); - m_btn1.disableAutoSize(); - getToolBar().addWidget(m_btn1, { 0, 0 }); + m_panel2.addWidget(m_btn1, { 0, 300 }); m_panel2.addWidget(m_img, { 20, 50 }); addWidget(m_tabs, { 0, 0 });