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)
}
% ===================
% ====== 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
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

View file

@ -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<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 });
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<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)
{
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();
}
}
}

View file

@ -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<Item*>& selectionList);
void add_selected(stdvec<Item*>& selectionList);
void remove_selected(stdvec<Item*>& 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<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:
Item InvalidItem { *this };
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)
{
if (isVScrollEnabled())

View file

@ -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;

View file

@ -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<ogfx::gui::widgets::ListView::Item*>& 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<bool> m_progressJob;