From fde961c78e933d59245384d3e5529642124f8a4f Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Thu, 16 Apr 2026 21:01:15 +0200 Subject: [PATCH] Added scrollbar widget --- CMakeLists.txt | 1 + extra/DefaultTheme.oss | 29 +++--- other/TODO.txt | 4 +- src/ogfx/gui/WidgetManager.cpp | 13 ++- src/ogfx/gui/Widgets.hpp | 1 + src/ogfx/gui/widgets/Containers.cpp | 54 ++++++---- src/ogfx/gui/widgets/Containers.hpp | 4 + src/ogfx/gui/widgets/Scrollbar.cpp | 147 ++++++++++++++++++++++++++++ src/ogfx/gui/widgets/Scrollbar.hpp | 72 ++++++++++++++ src/ogfx/gui/widgets/Widget.cpp | 10 +- src/ogfx/gui/widgets/Widget.hpp | 6 ++ src/ostd/math/Geometry.hpp | 2 +- src/test/GuiTest.cpp | 8 +- 13 files changed, 315 insertions(+), 36 deletions(-) create mode 100644 src/ogfx/gui/widgets/Scrollbar.cpp create mode 100644 src/ogfx/gui/widgets/Scrollbar.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b8632cf..129e27f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ list(APPEND OGFX_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Label.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/RootWidget.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/CheckBox.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Scrollbar.cpp # render ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/render/BasicRenderer.cpp diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index 159d397..699d286 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -40,22 +40,29 @@ window.backgroundColor = rgba(160, 160, 160, 255) % ====== Scrollbar ====== (scrollbar) { - width = 20 - (handle) { - color = #333333FF + (thumb) { + color = rgb(140, 140, 140) + borderRadius = 16 + borderColor = rgb(150, 150, 150) + showBorder = true } (track) { - color = #555555FF + color = rgb(90, 90, 90) + borderRadii = Rect(0, 0, 10, 0) + } + width = 18.0 +} +(scrollbar:hover) { + (thumb) { + color = #999999FF } } -(scrollbar:handle_hover) { - (handle) { - color = $accentColorDark +(scrollbar:pressed) { + (thumb) { + color = #999999FF } -} -(scrollbar:handle_pressed) { - (handle) { - color = $accentColorDark + (track) { + color = rgb(70, 70, 70) } } % ======================= diff --git a/other/TODO.txt b/other/TODO.txt index ac24cdd..c4e692e 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -18,9 +18,9 @@ ***Add triangle drawing functions to BasicRenderer2D ***Implement Panel title ***Implement show/hide functionality for widgets +***Create reliable default stylesheet Add theme caching Implement global scale (probably query desktop scale and adjust accordingly) -Create reliable default stylesheet Implement cursors in Stylesheet @@ -30,6 +30,7 @@ Implement following widgets: ***Checkbox ***Panel / Container ***Title Label + ***ScrollView Button Image/Icon Radio Button @@ -38,7 +39,6 @@ Implement following widgets: Horizontal Slider Vertical Slider Image / Icon - ScrollView ListBox ComboBox TreeView diff --git a/src/ogfx/gui/WidgetManager.cpp b/src/ogfx/gui/WidgetManager.cpp index facc208..7599b63 100644 --- a/src/ogfx/gui/WidgetManager.cpp +++ b/src/ogfx/gui/WidgetManager.cpp @@ -144,6 +144,7 @@ namespace ogfx if (!w->isVisible()) continue; if (!w->contains(event.mouse->position_x, event.mouse->position_y, true)) continue; + event.mouse->mousePressedOnWidget = w; w->__onMousePressed(event); m_mousePressedOnWidget = w; if (event.isHandled() || w->m_stopEvents) @@ -178,6 +179,12 @@ namespace ogfx void WidgetManager::onMouseMoved(const Event& event) { + if (m_mousePressedOnWidget != nullptr && + m_mousePressedOnWidget->m_pressedButton != MouseEventData::eButton::None && + !m_mousePressedOnWidget->contains(event.mouse->position_x, event.mouse->position_y, true)) + { + m_mousePressedOnWidget->__onMouseDragged(event); + } for (i32 i = m_widgetList.size() - 1; i >= 0; i--) { Widget* w = m_widgetList[i]; @@ -200,7 +207,7 @@ namespace ogfx } else { - if (w->m_pressedButton != ogfx::MouseEventData::eButton::None) + if (w->m_pressedButton != MouseEventData::eButton::None) w->__onMouseDragged(event); else w->__onMouseMoved(event); @@ -246,8 +253,8 @@ namespace ogfx if (w == nullptr) continue; if (w->isInvalid()) continue; if (!w->isVisible()) continue; - if (!w->contains(event.mouse->position_x, event.mouse->position_y, true)) - continue; + // if (!w->contains(event.mouse->position_x, event.mouse->position_y, true)) + // continue; w->__onMouseDragged(event); if (event.isHandled()) break; diff --git a/src/ogfx/gui/Widgets.hpp b/src/ogfx/gui/Widgets.hpp index 368239d..391c598 100644 --- a/src/ogfx/gui/Widgets.hpp +++ b/src/ogfx/gui/Widgets.hpp @@ -23,3 +23,4 @@ #include #include #include +#include diff --git a/src/ogfx/gui/widgets/Containers.cpp b/src/ogfx/gui/widgets/Containers.cpp index 5b83227..69551a4 100644 --- a/src/ogfx/gui/widgets/Containers.cpp +++ b/src/ogfx/gui/widgets/Containers.cpp @@ -64,8 +64,8 @@ namespace ogfx void Panel::onDraw(ogfx::BasicRenderer2D& gfx) { - if (isScrollAllowed()) - gfx.fillRect(getContentExtents() + Rectangle { getGlobalPosition(), 0, 0 }, { 80, 0, 0, 30 }); + // if (isScrollAllowed()) + // gfx.fillRect(getContentExtents() + Rectangle { getGlobalPosition(), 0, 0 }, { 80, 0, 0, 30 }); } void Panel::afterDraw(ogfx::BasicRenderer2D& gfx) @@ -77,21 +77,12 @@ namespace ogfx { if (!isScrollAllowed()) return; - auto ext = getContentExtents(); - auto cont = getContentBounds(); - f32 maxScroll = -(ext.h - cont.h); - if (event.mouse->scroll == MouseEventData::eScrollDirection::Down && m_scrollOffset.y > maxScroll) - { - m_scrollOffset.y -= (m_scrollSpeed.y * 15.0f); - if (m_scrollOffset.y < maxScroll) - m_scrollOffset.y = maxScroll; - } - else if (event.mouse->scroll == MouseEventData::eScrollDirection::Up && m_scrollOffset.y < 0) - { - m_scrollOffset.y += (m_scrollSpeed.y * 15.0f); - if (m_scrollOffset.y > 0) - m_scrollOffset.y = 0; - } + f32 offset_y = 0; + if (event.mouse->scroll == MouseEventData::eScrollDirection::Down) + offset_y = -(m_scrollSpeed.y * 15.0f); + else if (event.mouse->scroll == MouseEventData::eScrollDirection::Up) + offset_y = (m_scrollSpeed.y * 15.0f); + addScrollOffset({ 0, offset_y }); event.handle(); } @@ -126,6 +117,35 @@ namespace ogfx } } + void Panel::setScrollOffset(const Vec2& offset) + { + auto ext = getContentExtents(); + auto cont = getContentBounds(); + f32 maxScroll = -(ext.h - cont.h); + m_scrollOffset = offset; + if (m_scrollOffset.y < maxScroll) + m_scrollOffset.y = maxScroll; + if (m_scrollOffset.y > 0) + m_scrollOffset.y = 0; + } + + void Panel::addScrollOffset(const Vec2& offset) + { + auto ext = getContentExtents(); + auto cont = getContentBounds(); + f32 maxScroll = -(ext.h - cont.h); + m_scrollOffset += offset; + if (m_scrollOffset.y < maxScroll) + m_scrollOffset.y = maxScroll; + if (m_scrollOffset.y > 0) + m_scrollOffset.y = 0; + } + + bool Panel::needsScroll(void) const + { + return getContentExtents().h > getContentBounds().h; + } + void Panel::draw_titlebar(BasicRenderer2D& gfx) { f32 br = cast(getBorderWidth()); diff --git a/src/ogfx/gui/widgets/Containers.hpp b/src/ogfx/gui/widgets/Containers.hpp index 4f04c45..1cdf96a 100644 --- a/src/ogfx/gui/widgets/Containers.hpp +++ b/src/ogfx/gui/widgets/Containers.hpp @@ -49,11 +49,15 @@ namespace ogfx void onMouseScrolled(const Event& event) override; void setTitlebarType(const String& type); String getTitlebarType(void) const; + void setScrollOffset(const Vec2& offset) override; + void addScrollOffset(const Vec2& offset) override; + bool needsScroll(void) const override; inline void setBackGroundColor(const Color& color) { m_backgroundColor = color; } inline Color getBackgroundColor(void) { return m_backgroundColor; } inline String getTitle(void) const { return m_title; } inline void setTitle(const String& title) { m_title = title; } inline Vec2 getScrollOffset(void) const override { return m_scrollOffset; } + inline f32 getTitlebarHeight(void) const { return m_titlebarHeight; } private: void draw_titlebar(BasicRenderer2D& gfx); diff --git a/src/ogfx/gui/widgets/Scrollbar.cpp b/src/ogfx/gui/widgets/Scrollbar.cpp new file mode 100644 index 0000000..b8c4eb5 --- /dev/null +++ b/src/ogfx/gui/widgets/Scrollbar.cpp @@ -0,0 +1,147 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . +*/ + +#include "Scrollbar.hpp" +#include "../../render/BasicRenderer.hpp" + +namespace ogfx +{ + namespace gui + { + namespace widgets + { + VerticalScrollBar& VerticalScrollBar::create(const String& text) + { + setPadding({ 0, 0, 0, 0 }); + setMargin({ 0, 0, 0, 0 }); + setTypeName("ogfx::gui::widgets::Label"); + disableDrawBox(); + disableChildren(); + enableBackground(true); + enableBorder(false); + allowIgnoreScroll(true); + validate(); + return *this; + } + + void VerticalScrollBar::applyTheme(const ostd::Stylesheet& theme) + { + w = getThemeValue(theme, "scrollbar.width", 15); + m_thumbColor = getThemeValue(theme, "scrollbar.thumb.color", { 120, 120, 120 }); + m_thumbBorderRadius = getThemeValue(theme, "scrollbar.thumb.borderRadius", 16); + m_thumbBorderColor = getThemeValue(theme, "scrollbar.thumb.borderColor", { 150, 150, 150 }); + m_thumbShowBorder = getThemeValue(theme, "scrollbar.thumb.showBorder", true); + m_trackColor = getThemeValue(theme, "scrollbar.track.color", { 70, 70, 70 }); + m_trackBorderRadii = getThemeValue(theme, "scrollbar.track.borderRadii", { 0, 0, 10, 0 }); + } + + void VerticalScrollBar::afterDraw(ogfx::BasicRenderer2D& gfx) + { + if (!getParent()->needsScroll()) + return; + gfx.fillRoundRect(getGlobalBounds() - m_correctionOffset, m_trackColor, m_trackBorderRadii); + gfx.outlinedRoundRect(m_thumbGlobalBounds, m_thumbColor, m_thumbBorderColor, m_thumbBorderRadius, 1); + } + + void VerticalScrollBar::onMouseDragged(const Event& event) + { + if (!m_mousePressed) + return; + + if (is_mouse_in_thumb({ event.mouse->position_x, event.mouse->position_y })) + m_mouseDragged = true; + + if (m_mouseDragged) + { + auto bounds = getGlobalBounds() - m_correctionOffset; + f32 newThumbY = (event.mouse->position_y - bounds.y) - m_dragGrabOffset; + set_thumb_y(newThumbY); + } + } + + void VerticalScrollBar::onMousePressed(const Event& event) + { + m_mousePressed = true; + m_dragGrabOffset = event.mouse->position_y - m_thumbGlobalBounds.y; + } + + void VerticalScrollBar::onMouseReleased(const Event& event) + { + m_mousePressed = false; + if (event.mouse->mousePressedOnWidget != this) + return; + if (!m_mouseDragged) + { + auto bounds = getGlobalBounds() - m_correctionOffset; + f32 newThumbY = (event.mouse->position_y - bounds.y); + newThumbY -= m_thumbHeight / 2.0f; + set_thumb_y(newThumbY); + } + m_mouseDragged = false; + } + + void VerticalScrollBar::onUpdate(void) + { + update_thumb(); + } + + void VerticalScrollBar::update_thumb(void) + { + x = getParent()->getSize().x - w; + y = 0; + h = getParent()->getGlobalContentBounds().h; + + auto ext = getParent()->getContentExtents(); + auto cont = getParent()->getContentBounds(); + auto scrollOfset = getParent()->getScrollOffset(); + + // Thumb size is proportional to visible/total ratio + f32 visibleRatio = cont.h / ext.h; // e.g. 0.4 means 40% visible + m_thumbHeight = std::max(20.0f, geth() * visibleRatio); // min 20px for usability + + // Thumb position is proportional to scroll progress + f32 scrollProgress = -scrollOfset.y / (ext.h - cont.h); // 0 to 1 + m_thumbY = gety() + ((geth() - m_thumbHeight) * scrollProgress); + + auto bounds = getGlobalBounds() - m_correctionOffset; + m_thumbGlobalBounds = { bounds.x + 2, bounds.y + gety() + m_thumbY, getw() - 4, m_thumbHeight }; + } + + void VerticalScrollBar::set_thumb_y(f32 thumby) + { + auto bounds = getGlobalBounds() - m_correctionOffset; + auto ext = getParent()->getContentExtents(); + auto cont = getParent()->getContentBounds(); + thumby = std::clamp(thumby, 0.0f, h - m_thumbHeight); + m_thumbY = thumby; + f32 scrollProgress = (bounds.y + gety() + m_thumbY - bounds.y) / (bounds.h - m_thumbHeight); + f32 maxScroll = -(ext.h - cont.h); + auto parentScrollOffset = getParent()->getScrollOffset(); + parentScrollOffset.y = maxScroll * scrollProgress; + getParent()->setScrollOffset(parentScrollOffset); + } + + bool VerticalScrollBar::is_mouse_in_thumb(const Vec2& mouse_pos) + { + return m_thumbGlobalBounds.contains(mouse_pos, true); + } + } + } +} diff --git a/src/ogfx/gui/widgets/Scrollbar.hpp b/src/ogfx/gui/widgets/Scrollbar.hpp new file mode 100644 index 0000000..8fb976c --- /dev/null +++ b/src/ogfx/gui/widgets/Scrollbar.hpp @@ -0,0 +1,72 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . +*/ + +#pragma once + +#include + +namespace ogfx +{ + namespace gui + { + namespace widgets + { + class VerticalScrollBar : public Widget + { + public: + inline VerticalScrollBar(WindowCore& window) : Widget({ 0, 0, 0, 0 }, window) { create(""); } + inline VerticalScrollBar(WindowCore& window, const String& text) : Widget({ 0, 0, 0, 0 }, window) { create(text); } + VerticalScrollBar& create(const String& text); + void applyTheme(const ostd::Stylesheet& theme) override; + void afterDraw(ogfx::BasicRenderer2D& gfx) override; + void onMouseDragged(const Event& event) override; + void onMousePressed(const Event& event) override; + void onMouseReleased(const Event& event) override; + void onUpdate(void) override; + + inline void setx(f32 xx) override { } + inline void sety(f32 yy) override { } + inline void setw(f32 ww) override { } + inline void seth(f32 hh) override { } + + private: + void update_thumb(void); + void set_thumb_y(f32 thumby); + bool is_mouse_in_thumb(const Vec2& mouse_pos); + + private: + f32 m_thumbHeight { 0 }; + f32 m_thumbY { 0 }; + Rectangle m_thumbGlobalBounds { 0, 0, 0, 0 }; + bool m_mousePressed { false }; + bool m_mouseDragged { false }; + f32 m_dragGrabOffset { 0 }; + Rectangle m_correctionOffset { 1, 1, 0, 0 }; + + Rectangle m_trackBorderRadii { 0, 0, 10, 0 }; + f32 m_thumbBorderRadius { 16 }; + Color m_trackColor { 70, 70, 70 }; + Color m_thumbColor { 120, 120, 120 }; + Color m_thumbBorderColor { 150, 150, 150 }; + bool m_thumbShowBorder { true }; + }; + } + } +} diff --git a/src/ogfx/gui/widgets/Widget.cpp b/src/ogfx/gui/widgets/Widget.cpp index 8597e22..7ba7d9f 100644 --- a/src/ogfx/gui/widgets/Widget.cpp +++ b/src/ogfx/gui/widgets/Widget.cpp @@ -50,7 +50,14 @@ namespace ogfx { Vec2 glob = getPosition(); if (!m_rootChild && m_parent != nullptr) - glob += m_parent->getGlobalPosition() + m_parent->getPadding().getPosition() + m_parent->getScrollOffset(); + { + glob += m_parent->getGlobalPosition(); + if (!isIgnoreScrollAllowed()) + { + glob += m_parent->getPadding().getPosition(); + glob += m_parent->getScrollOffset(); + } + } glob += m_margin.getPosition(); return glob; } @@ -83,6 +90,7 @@ namespace ogfx for (auto* child : m_widgets.getWidgets()) { if (!child || child->isInvalid()) continue; + if (child->isIgnoreScrollAllowed()) continue; Vec2 localPos = getPadding().getPosition() + child->getPosition() + child->getMargin().getPosition() + child->getContentBounds().getPosition(); maxX = std::max(maxX, localPos.x + child->getw() + child->getMargin().w); maxY = std::max(maxY, localPos.y + child->geth() + child->getMargin().h); diff --git a/src/ogfx/gui/widgets/Widget.hpp b/src/ogfx/gui/widgets/Widget.hpp index 8b7c215..82d855c 100644 --- a/src/ogfx/gui/widgets/Widget.hpp +++ b/src/ogfx/gui/widgets/Widget.hpp @@ -53,6 +53,9 @@ namespace ogfx void enable(bool enable = true); virtual void applyTheme(const ostd::Stylesheet& theme) = 0; inline virtual Vec2 getScrollOffset(void) const { return { 0, 0 }; } + inline virtual void setScrollOffset(const Vec2& offset) { } + inline virtual void addScrollOffset(const Vec2& offset) { } + inline virtual bool needsScroll(void) const { return false; } void addThemeOverride(const String& fullKey, ostd::Stylesheet::TypeVariant value); void reloadTheme(bool propagate = false); void setThemeQualifier(const String& qualifier, bool value = true); @@ -161,6 +164,8 @@ namespace ogfx inline void hide(void) { setVisible(false); } inline void allowScroll(bool allow = true) { m_allowScroll = allow; } inline bool isScrollAllowed(void) const { return m_allowScroll; } + inline void allowIgnoreScroll(bool allow = true) { m_ignoreScroll = allow; } + inline bool isIgnoreScrollAllowed(void) const { return m_ignoreScroll; } template inline T getThemeValue(const ostd::Stylesheet &theme, const String& key, const T& fallback) @@ -221,6 +226,7 @@ namespace ogfx bool m_acceptDragAndDrop { false }; bool m_visible { true }; bool m_allowScroll { false }; + bool m_ignoreScroll { false}; MouseEventData::eButton m_pressedButton { MouseEventData::eButton::None }; stdvec m_themeIDList; diff --git a/src/ostd/math/Geometry.hpp b/src/ostd/math/Geometry.hpp index 464e966..6418b34 100755 --- a/src/ostd/math/Geometry.hpp +++ b/src/ostd/math/Geometry.hpp @@ -323,7 +323,7 @@ namespace ostd inline virtual Vec2 getSize(void) const { return Vec2(getw(), geth()); } inline virtual Vec2 getCenter(void) const { return Vec2(getx() + getw() / 2, gety() + geth() / 2); } - inline virtual void setPosition(Vec2 pos) { setx(pos.x); sety(pos.y); } + inline virtual void setPosition(const Vec2& pos) { setx(pos.x); sety(pos.y); } inline virtual void setPosition(f32 xx, f32 yy) { setx(xx); sety(yy); } inline virtual void setSize(Vec2 size) { setw(size.x); seth(size.y); } inline virtual void setSize(f32 ww, f32 hh) { setw(ww); seth(hh); } diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index c27d8b8..545e979 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -50,6 +50,7 @@ class Window : public ogfx::gui::Window m_label1.setText("Show Panel2"); m_label1.setMousePressedCallback([&](const ogfx::gui::Event& event) -> void { m_check1.setChecked(!m_check1.isChecked()); + m_label1.addy(10); }); m_label1.addThemeOverride("@.label.showBackground", true); m_label1.addThemeOverride("@.label.backgroundColor", Colors::Beige); @@ -75,11 +76,14 @@ class Window : public ogfx::gui::Window m_panel2.setSize(600, 400); + m_scroll.setMargin({ 0, m_panel2.getTitlebarHeight(), 0, 0 }); + m_panel1.addWidget(m_label2); m_panel2.addWidget(m_label3); m_panel2.addWidget(m_panel1, { 0, 50 }); - m_panel2.addWidget(m_label1, { 0, 340 }); + m_panel2.addWidget(m_label1, { 0, 500 }); + m_panel2.addWidget(m_scroll); addWidget(m_check1, { 30, 30 }); addWidget(m_panel2, { 30, 100 }); @@ -115,6 +119,8 @@ class Window : public ogfx::gui::Window ogfx::gui::widgets::Panel m_panel1 { *this }; ogfx::gui::widgets::Panel m_panel2 { *this }; ogfx::gui::widgets::CheckBox m_check1 { *this }; + ogfx::gui::widgets::VerticalScrollBar m_scroll { *this }; + ostd::Stylesheet m_theme; ogfx::Animation m_anim; ogfx::Image m_img;