Added ListView widget

This commit is contained in:
OmniaX-Dev 2026-04-23 13:41:09 +02:00
parent 69e7fb7f1d
commit 183df32659
7 changed files with 226 additions and 19 deletions

View file

@ -255,3 +255,22 @@ const $accentColorDark = #6B0A1DFF
padding = rect(5, 5, 5, 5) 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
}
% ===================

View file

@ -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 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 buttons to scroll tabs in TabPanel
Add Font class, to handle different font attributes 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 ***Progressbar
***Horizontal Slider ***Horizontal Slider
***Vertical Slider ***Vertical Slider
ListBox ***ListBox
ComboBox ComboBox
Context Menu Context Menu
Menu Bar Menu Bar

View file

@ -21,7 +21,7 @@
#include "ListView.hpp" #include "ListView.hpp"
#include "../../render/BasicRenderer.hpp" #include "../../render/BasicRenderer.hpp"
#include "../Window.hpp" #include "../Window.hpp"
#include "../../../ostd/math/Random.hpp" #include "../../../ostd/io/Memory.hpp"
namespace ogfx namespace ogfx
{ {
@ -53,6 +53,31 @@ namespace ogfx
m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h }; m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h };
} }
void ListView::Item::set_selected(stdvec<Item*>& selectionList)
{
for (auto& sel : selectionList)
sel->m_selected = false;
selectionList.clear();
selectionList.push_back(this);
m_selected = true;
}
void ListView::Item::add_selected(stdvec<Item*>& selectionList)
{
if (STDVEC_CONTAINS(selectionList, this))
return;
selectionList.push_back(this);
m_selected = true;
}
void ListView::Item::remove_selected(stdvec<Item*>& selectionList)
{
if (!STDVEC_CONTAINS(selectionList, this))
return;
STDVEC_REMOVE(selectionList, this);
m_selected = false;
}
@ -67,29 +92,78 @@ namespace ogfx
setBorderColor({ 50, 50, 50 }); setBorderColor({ 50, 50, 50 });
setStylesheetCategoryName("list"); setStylesheetCategoryName("list");
validate(); validate();
for (i32 i = 0; i < 30; i++)
{
Item k { *this };
m_list.push_back(k);
}
return *this; return *this;
} }
void ListView::applyTheme(const ostd::Stylesheet& theme) void ListView::applyTheme(const ostd::Stylesheet& theme)
{ {
setSeparatorLineColor(getThemeValue<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
setDefaultLineTextColor(getThemeValue<Color>(theme, "item.defaultTextColor", getDefaultLineTextColor()));
setDefaultLineFontSize(getThemeValue<i32>(theme, "item.defaultFontSize", getDefaultLineFontSize()));
setDefaultLinePadding(getThemeValue<Rectangle>(theme, "item.defaultPadding", getDefaultLinePadding()));
setDefaultLineSelectionColor(getThemeValue<Color>(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor()));
setDefaultLineSelectionTextColor(getThemeValue<Color>(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor()));
} }
void ListView::onDraw(ogfx::BasicRenderer2D& gfx) void ListView::onDraw(ogfx::BasicRenderer2D& gfx)
{ {
f32 y = 0; // f32 y = 0;
const auto& bounds = getGlobalBounds(); 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) for (auto& item : m_list)
{ {
if (!item.isValid()) continue; if (!item.isValid()) { startIdx++; continue; }
const auto& size = item.getDimensions(); f32 itemH = item.getDimensions().y;
gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), item.getTextColor(), item.getFontSize()); if (y + itemH > visibleStart) break; // this item is the first one in view
y += size.y; 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) void ListView::afterDraw(ogfx::BasicRenderer2D& gfx)
@ -97,6 +171,35 @@ namespace ogfx
drawScrollbars(gfx); 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 Rectangle ListView::getContentExtents(void) const
{ {
f32 maxX = 0, maxY = 0; f32 maxX = 0, maxY = 0;
@ -110,15 +213,34 @@ namespace ogfx
return { 0, 0, maxX, maxY }; return { 0, 0, maxX, maxY };
} }
void ListView::refresh(void) ListView::Item& ListView::getItem(const String& text)
{ {
for (auto& item : m_list) for (auto& item : m_list)
{ if (item.getText() == text)
item.setText(ostd::Random::getString(ostd::Random::getui8(0, 40))); return item;
return InvalidItem;
} }
m_list[10].setFontSize(40); ListView::Item& ListView::getItem(u32 index)
m_list[16].setTextColor(Colors::Crimson); {
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();
} }
} }
} }

View file

@ -44,21 +44,31 @@ namespace ogfx
inline Vec2 getDimensions(void) const { return m_dimensions; } inline Vec2 getDimensions(void) const { return m_dimensions; }
inline i32 getFontSize(void) const { return m_fontSize; } inline i32 getFontSize(void) const { return m_fontSize; }
inline String getText(void) const { return m_text; } 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(Color, TextColor, m_textColor);
OSTD_PARAM_GETSET(Rectangle, Padding, m_padding); OSTD_PARAM_GETSET(Rectangle, Padding, m_padding);
OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor);
OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor);
private: private:
void update_dimensions(void); void update_dimensions(void);
void set_selected(stdvec<Item*>& selectionList);
void add_selected(stdvec<Item*>& selectionList);
void remove_selected(stdvec<Item*>& selectionList);
private: private:
Vec2 m_dimensions { 0, 0 }; Vec2 m_dimensions { 0, 0 };
bool m_selected { false };
ListView* m_parent { nullptr }; ListView* m_parent { nullptr };
String m_text { "" }; String m_text { "" };
i32 m_fontSize { 16 }; i32 m_fontSize { 16 };
Color m_textColor { 30, 30, 30 }; Color m_textColor { 30, 30, 30 };
Color m_selectedTextColor { Colors::White };
Color m_selectedColor { Colors::Crimson };
Rectangle m_padding { 5, 5, 20, 0 }; Rectangle m_padding { 5, 5, 20, 0 };
friend class ListView;
}; };
public: public:
inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); } inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); }
@ -66,11 +76,35 @@ namespace ogfx
void applyTheme(const ostd::Stylesheet& theme) override; void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override; void onDraw(ogfx::BasicRenderer2D& gfx) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override; void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
Rectangle getContentExtents(void) const 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<Item*>& getSelection(void) { return m_selectedList; }
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& 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: private:
Item InvalidItem { *this };
stdvec<Item> m_list; stdvec<Item> m_list;
stdvec<Item*> m_selectedList;
std::function<void(stdvec<Item*>& 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 };
}; };
} }
} }

View file

@ -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) void ScrollableWidget::onMouseScrolled(const Event& event)
{ {
if (isVScrollEnabled()) if (isVScrollEnabled())

View file

@ -114,6 +114,9 @@ namespace ogfx
void drawScrollbars(ogfx::BasicRenderer2D& gfx); void drawScrollbars(ogfx::BasicRenderer2D& gfx);
void updateScrollbarsSize(void); void updateScrollbarsSize(void);
void resetScroll(bool horizontal = true, bool vertical = true, bool propagate = true) override; 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 onMouseScrolled(const Event& event) override;
virtual void setScrollOffset(const Vec2& offset) override; virtual void setScrollOffset(const Vec2& offset) override;
virtual void addScrollOffset(const Vec2& offset) override; virtual void addScrollOffset(const Vec2& offset) override;

View file

@ -114,8 +114,21 @@ class Window : public ogfx::gui::Window
}); });
m_slideLbl.setText(String("").add(m_slide.getValue(), 2)); 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); 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<ogfx::gui::widgets::ListView::Item*>& selection) -> void {\
std::cout << *(selection[0]) << "\n";
});
m_tabs.setSize(900, 700); m_tabs.setSize(900, 700);
auto& t1 = m_tabs.addTab("Tab1"); auto& t1 = m_tabs.addTab("Tab1");
auto& t2 = m_tabs.addTab("Tab2 Test"); auto& t2 = m_tabs.addTab("Tab2 Test");
@ -151,6 +164,7 @@ class Window : public ogfx::gui::Window
m_panel2.addWidget(m_img, { 20, 50 }); m_panel2.addWidget(m_img, { 20, 50 });
addWidget(m_tabs, { 0, 0 }); addWidget(m_tabs, { 0, 0 });
addWidget(m_drawCallsLbl, { 39, m_tabs.getSize().y + 30 });
m_theme.loadFromFile("./DefaultTheme.oss", true, getDefaultStylesheetVariableList()); m_theme.loadFromFile("./DefaultTheme.oss", true, getDefaultStylesheetVariableList());
setTheme(m_theme); setTheme(m_theme);
@ -180,6 +194,7 @@ class Window : public ogfx::gui::Window
void onRedraw(ogfx::BasicRenderer2D& gfx) override void onRedraw(ogfx::BasicRenderer2D& gfx) override
{ {
// gfx.fillRect(m_tabs.getGlobalPureContentBounds(), { 0, 255, 0, 100 }); // gfx.fillRect(m_tabs.getGlobalPureContentBounds(), { 0, 255, 0, 100 });
m_drawCallsLbl.setText(String("").add(gfx.getDrawCallCount()));
} }
void onFixedUpdate(void) override void onFixedUpdate(void) override
@ -204,6 +219,7 @@ class Window : public ogfx::gui::Window
Slider m_slide { *this }; Slider m_slide { *this };
Label m_slideLbl { *this }; Label m_slideLbl { *this };
ListView m_list { *this }; ListView m_list { *this };
Label m_drawCallsLbl { *this };
ostd::StepTimer m_timer; ostd::StepTimer m_timer;
ostd::AsyncJob<bool> m_progressJob; ostd::AsyncJob<bool> m_progressJob;