diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index ee33fa2..c3ce018 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -255,3 +255,22 @@ const $accentColorDark = #6B0A1DFF padding = rect(5, 5, 5, 5) } % =================== + + + +% ====== Slider ====== +(list) { + (item) { + defaultTextColor = #202020FF + defaultFontSize = 16 + defaultPadding = rect(5, 5, 20, 0) + defaultSelectionColor = $accentColor + defaultSelectionTextColor = color_white + } + backgroundColor = rgb(160, 160, 160) + borderColor = rgb(50, 50, 50) + separatorLineColor = rgb(40, 40, 40) + showSeparatorLine = true + +} +% =================== diff --git a/other/TODO.txt b/other/TODO.txt index 3669822..bcb7989 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -29,6 +29,9 @@ FIX: Float values in stylesheet needing to have decimal point to be read as floa Make parsing of vec2 and rect in stylesheet, consistent with other value functions like gradients and animations Add buttons to scroll tabs in TabPanel Add Font class, to handle different font attributes +Add multi-selection to ListView +Cache getStringDimensions() call in TabPanel::draw_tabs +FIX: Optimize ListView @@ -49,7 +52,7 @@ Implement following widgets: ***Progressbar ***Horizontal Slider ***Vertical Slider - ListBox + ***ListBox ComboBox Context Menu Menu Bar diff --git a/src/ogfx/gui/widgets/ListView.cpp b/src/ogfx/gui/widgets/ListView.cpp index e2a1abd..01ac34f 100644 --- a/src/ogfx/gui/widgets/ListView.cpp +++ b/src/ogfx/gui/widgets/ListView.cpp @@ -21,7 +21,7 @@ #include "ListView.hpp" #include "../../render/BasicRenderer.hpp" #include "../Window.hpp" -#include "../../../ostd/math/Random.hpp" +#include "../../../ostd/io/Memory.hpp" namespace ogfx { @@ -53,6 +53,31 @@ namespace ogfx m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h }; } + void ListView::Item::set_selected(stdvec& selectionList) + { + for (auto& sel : selectionList) + sel->m_selected = false; + selectionList.clear(); + selectionList.push_back(this); + m_selected = true; + } + + void ListView::Item::add_selected(stdvec& selectionList) + { + if (STDVEC_CONTAINS(selectionList, this)) + return; + selectionList.push_back(this); + m_selected = true; + } + + void ListView::Item::remove_selected(stdvec& selectionList) + { + if (!STDVEC_CONTAINS(selectionList, this)) + return; + STDVEC_REMOVE(selectionList, this); + m_selected = false; + } + @@ -67,29 +92,78 @@ namespace ogfx setBorderColor({ 50, 50, 50 }); setStylesheetCategoryName("list"); validate(); - for (i32 i = 0; i < 30; i++) - { - Item k { *this }; - m_list.push_back(k); - } return *this; } void ListView::applyTheme(const ostd::Stylesheet& theme) { + setSeparatorLineColor(getThemeValue(theme, "separatorLineColor", getSeparatorLineColor())); + enableShowSeparatorLine(getThemeValue(theme, "showSeparatorLine", isShowSeparatorLineEnabled())); + setDefaultLineTextColor(getThemeValue(theme, "item.defaultTextColor", getDefaultLineTextColor())); + setDefaultLineFontSize(getThemeValue(theme, "item.defaultFontSize", getDefaultLineFontSize())); + setDefaultLinePadding(getThemeValue(theme, "item.defaultPadding", getDefaultLinePadding())); + setDefaultLineSelectionColor(getThemeValue(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor())); + setDefaultLineSelectionTextColor(getThemeValue(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor())); } void ListView::onDraw(ogfx::BasicRenderer2D& gfx) { - f32 y = 0; + // f32 y = 0; const auto& bounds = getGlobalBounds(); + const auto& content = getContentExtents(); + Color textColor; + // Compute the visible Y window in content space + const f32 scrollY = -getScrollOffset().y; // positive offset into content + const f32 visibleH = getContentBounds().h; + const f32 visibleStart = scrollY; + const f32 visibleEnd = scrollY + visibleH; + + // Find the starting item and its Y via a linear scan... + f32 y = 0; + i32 startIdx = 0; for (auto& item : m_list) { - if (!item.isValid()) continue; - const auto& size = item.getDimensions(); - gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), item.getTextColor(), item.getFontSize()); - y += size.y; + if (!item.isValid()) { startIdx++; continue; } + f32 itemH = item.getDimensions().y; + if (y + itemH > visibleStart) break; // this item is the first one in view + y += itemH; + startIdx++; } + + // ...then process only items from startIdx until we exceed visibleEnd + for (i32 i = startIdx; i < (i32)m_list.size(); i++) + { + auto& item = m_list[i]; + if (!item.isValid()) continue; + f32 itemH = item.getDimensions().y; + if (y > visibleEnd) break; // past the visible window, stop + if (item.isSelected()) + { + textColor = item.getSelectedTextColor(); + gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + 4 } + getScrollOffset(), { content.w, itemH } }, item.getSelectedColor()); + } + else + textColor = item.getTextColor(); + gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + 4 } + getScrollOffset(), Vec2 { bounds.x + content.w, bounds.y + y + itemH + 4 } + getScrollOffset() }, Colors::Black, 1); + gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize()); + // draw or hit-test item at content Y = y + y += itemH; + } + // for (auto& item : m_list) + // { + // if (!item.isValid()) continue; + // const auto& size = item.getDimensions(); + // if (item.isSelected()) + // { + // textColor = item.getSelectedTextColor(); + // gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + 4 } + getScrollOffset(), { content.w, size.y } }, item.getSelectedColor()); + // } + // else + // textColor = item.getTextColor(); + // gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + size.y + 4 } + getScrollOffset(), Vec2 { bounds.x + content.w, bounds.y + y + size.y + 4 } + getScrollOffset() }, Colors::Black, 1); + // gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize()); + // y += size.y; + // } } void ListView::afterDraw(ogfx::BasicRenderer2D& gfx) @@ -97,6 +171,35 @@ namespace ogfx drawScrollbars(gfx); } + void ListView::onMouseReleased(const Event& event) + { + if (event.mouse->button != ogfx::MouseEventData::eButton::Left) + return; + if (isMouseInsideAnyScrollbar()) + return; + + const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; + const Vec2 origin = getGlobalBounds().getPosition(); + const Vec2 scroll = getScrollOffset(); + const f32 localY = mousePos.y - origin.y - scroll.y; + + f32 y = 0; + for (auto& item : m_list) + { + if (!item.isValid()) continue; + const f32 itemH = item.getDimensions().y; + if (!item.isSelected() && localY >= y && localY < y + itemH) + { + item.set_selected(m_selectedList); + if (callback_onSelectionChanged) + callback_onSelectionChanged(m_selectedList); + event.handle(); + break; + } + y += itemH; + } + } + Rectangle ListView::getContentExtents(void) const { f32 maxX = 0, maxY = 0; @@ -110,15 +213,34 @@ namespace ogfx return { 0, 0, maxX, maxY }; } - void ListView::refresh(void) + ListView::Item& ListView::getItem(const String& text) { for (auto& item : m_list) - { - item.setText(ostd::Random::getString(ostd::Random::getui8(0, 40))); - } + if (item.getText() == text) + return item; + return InvalidItem; + } - m_list[10].setFontSize(40); - m_list[16].setTextColor(Colors::Crimson); + ListView::Item& ListView::getItem(u32 index) + { + if (index < m_list.size()) + return m_list[index]; + return InvalidItem; + } + + ListView::Item& ListView::addLine(const String& text) + { + Item item { *this }; + item.setTextColor(m_defaultLinetextColor); + item.setPadding(m_defaultLinepadding); + item.setFontSize(m_defaultLinefontSize); + item.setSelectedColor(m_defaultSelectionColor); + item.setSelectedTextColor(m_defaultSelectionTextColor); + item.setText(text); + m_list.push_back(item); + if (m_list.size() == 1) + m_list.back().set_selected(m_selectedList); + return m_list.back(); } } } diff --git a/src/ogfx/gui/widgets/ListView.hpp b/src/ogfx/gui/widgets/ListView.hpp index 082b317..5af48c0 100644 --- a/src/ogfx/gui/widgets/ListView.hpp +++ b/src/ogfx/gui/widgets/ListView.hpp @@ -44,21 +44,31 @@ namespace ogfx inline Vec2 getDimensions(void) const { return m_dimensions; } inline i32 getFontSize(void) const { return m_fontSize; } inline String getText(void) const { return m_text; } + inline bool isSelected(void) const { return m_selected; } OSTD_PARAM_GETSET(Color, TextColor, m_textColor); OSTD_PARAM_GETSET(Rectangle, Padding, m_padding); + OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor); + OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor); private: void update_dimensions(void); + void set_selected(stdvec& selectionList); + void add_selected(stdvec& selectionList); + void remove_selected(stdvec& selectionList); private: Vec2 m_dimensions { 0, 0 }; + bool m_selected { false }; ListView* m_parent { nullptr }; String m_text { "" }; i32 m_fontSize { 16 }; Color m_textColor { 30, 30, 30 }; + Color m_selectedTextColor { Colors::White }; + Color m_selectedColor { Colors::Crimson }; Rectangle m_padding { 5, 5, 20, 0 }; + friend class ListView; }; public: inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); } @@ -66,11 +76,35 @@ namespace ogfx void applyTheme(const ostd::Stylesheet& theme) override; void onDraw(ogfx::BasicRenderer2D& gfx) override; void afterDraw(ogfx::BasicRenderer2D& gfx) override; + void onMouseReleased(const Event& event) override; Rectangle getContentExtents(void) const override; - void refresh(void) override; + Item& getItem(const String& text); + Item& getItem(u32 index); + Item& addLine(const String& text); + inline void addLine(const Item& line) { m_list.push_back(line); if (m_list.size() == 1) m_list.back().set_selected(m_selectedList); } + inline stdvec& getSelection(void) { return m_selectedList; } + inline void setSelectionChangedCallback(std::function& selection)> callback) { callback_onSelectionChanged = std::move(callback); } + + OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor); + OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine); + OSTD_PARAM_GETSET(i32, DefaultLineFontSize, m_defaultLinefontSize); + OSTD_PARAM_GETSET(Color, DefaultLineTextColor, m_defaultLinetextColor); + OSTD_PARAM_GETSET(Rectangle, DefaultLinePadding, m_defaultLinepadding); + OSTD_PARAM_GETSET(Color, DefaultLineSelectionColor, m_defaultSelectionColor); + OSTD_PARAM_GETSET(Color, DefaultLineSelectionTextColor, m_defaultSelectionTextColor); private: + Item InvalidItem { *this }; stdvec m_list; + stdvec m_selectedList; + std::function& selection)> callback_onSelectionChanged { nullptr }; + Color m_lineColor { 40, 40, 40 }; + bool m_showLine { true }; + i32 m_defaultLinefontSize { 16 }; + Color m_defaultLinetextColor { 30, 30, 30 }; + Rectangle m_defaultLinepadding { 5, 5, 20, 0 }; + Color m_defaultSelectionColor { Colors::Crimson }; + Color m_defaultSelectionTextColor { Colors::White }; }; } } diff --git a/src/ogfx/gui/widgets/Scrollbar.cpp b/src/ogfx/gui/widgets/Scrollbar.cpp index 91475c3..00bcebd 100644 --- a/src/ogfx/gui/widgets/Scrollbar.cpp +++ b/src/ogfx/gui/widgets/Scrollbar.cpp @@ -355,6 +355,16 @@ namespace ogfx } } + bool ScrollableWidget::isMouseInsideVScrollbar(void) const + { + return m_vScrollbar.isMouseInside(); + } + + bool ScrollableWidget::isMouseInsideHScrollbar(void) const + { + return m_hScrollbar.isMouseInside(); + } + void ScrollableWidget::onMouseScrolled(const Event& event) { if (isVScrollEnabled()) diff --git a/src/ogfx/gui/widgets/Scrollbar.hpp b/src/ogfx/gui/widgets/Scrollbar.hpp index 4e30a0a..b8e9216 100644 --- a/src/ogfx/gui/widgets/Scrollbar.hpp +++ b/src/ogfx/gui/widgets/Scrollbar.hpp @@ -114,6 +114,9 @@ namespace ogfx void drawScrollbars(ogfx::BasicRenderer2D& gfx); void updateScrollbarsSize(void); void resetScroll(bool horizontal = true, bool vertical = true, bool propagate = true) override; + bool isMouseInsideVScrollbar(void) const; + bool isMouseInsideHScrollbar(void) const; + inline bool isMouseInsideAnyScrollbar(void) const { return isMouseInsideVScrollbar() || isMouseInsideHScrollbar(); } virtual void onMouseScrolled(const Event& event) override; virtual void setScrollOffset(const Vec2& offset) override; virtual void addScrollOffset(const Vec2& offset) override; diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 9e8d806..4e9da71 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -114,8 +114,21 @@ class Window : public ogfx::gui::Window }); m_slideLbl.setText(String("").add(m_slide.getValue(), 2)); + m_drawCallsLbl.addThemeOverride("@.label.fontSize", 30); + m_drawCallsLbl.addThemeOverride("@.label.textColor", Colors::Crimson); + m_list.setSize(200, 300); + for (i32 i = 0; i < 100; i++) + { + m_list.addLine(ostd::Random::getString(ostd::Random::getui8(0, 40))); + } + m_list.getItem(10).setFontSize(40); + m_list.getItem(160).setTextColor(Colors::Crimson); + m_list.setSelectionChangedCallback([&](stdvec& selection) -> void {\ + std::cout << *(selection[0]) << "\n"; + }); + m_tabs.setSize(900, 700); auto& t1 = m_tabs.addTab("Tab1"); auto& t2 = m_tabs.addTab("Tab2 Test"); @@ -151,6 +164,7 @@ class Window : public ogfx::gui::Window m_panel2.addWidget(m_img, { 20, 50 }); addWidget(m_tabs, { 0, 0 }); + addWidget(m_drawCallsLbl, { 39, m_tabs.getSize().y + 30 }); m_theme.loadFromFile("./DefaultTheme.oss", true, getDefaultStylesheetVariableList()); setTheme(m_theme); @@ -180,6 +194,7 @@ class Window : public ogfx::gui::Window void onRedraw(ogfx::BasicRenderer2D& gfx) override { // gfx.fillRect(m_tabs.getGlobalPureContentBounds(), { 0, 255, 0, 100 }); + m_drawCallsLbl.setText(String("").add(gfx.getDrawCallCount())); } void onFixedUpdate(void) override @@ -204,6 +219,7 @@ class Window : public ogfx::gui::Window Slider m_slide { *this }; Label m_slideLbl { *this }; ListView m_list { *this }; + Label m_drawCallsLbl { *this }; ostd::StepTimer m_timer; ostd::AsyncJob m_progressJob;